feat(console): refactor Program.cs and add DXF file import support

Refactor flat top-level statements into NestConsole class with Options
and focused methods. Add support for passing .dxf files directly as
input — auto-imports geometry via DxfImporter and creates a fresh nest
with a plate when --size is specified. Supports three modes: nest-only,
DXF-only (requires --size), and mixed nest+DXF.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-14 21:50:01 -04:00
parent 9783d417bd
commit eddcc7602d
+283 -129
View File
@@ -4,190 +4,325 @@ using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using OpenNest; using OpenNest;
using OpenNest.Converters;
using OpenNest.Geometry; using OpenNest.Geometry;
using OpenNest.IO; using OpenNest.IO;
// Parse arguments. return NestConsole.Run(args);
var nestFile = (string)null;
var drawingName = (string)null; static class NestConsole
var plateIndex = 0; {
var outputFile = (string)null; public static int Run(string[] args)
var quantity = 0; {
var spacing = (double?)null; var options = ParseArgs(args);
var plateWidth = (double?)null;
var plateHeight = (double?)null; if (options == null)
var checkOverlaps = false; return 0; // --help was requested
var noSave = false;
var noLog = false; if (options.InputFiles.Count == 0)
var keepParts = false; {
var autoNest = false; PrintUsage();
var templateFile = (string)null; return 1;
}
foreach (var f in options.InputFiles)
{
if (!File.Exists(f))
{
Console.Error.WriteLine($"Error: file not found: {f}");
return 1;
}
}
using var log = SetUpLog(options);
var nest = LoadOrCreateNest(options);
if (nest == null)
return 1;
var plate = nest.Plates[options.PlateIndex];
ApplyTemplate(plate, options);
ApplyOverrides(plate, options);
var drawing = ResolveDrawing(nest, options);
if (drawing == null)
return 1;
var existingCount = plate.Parts.Count;
if (!options.KeepParts)
plate.Parts.Clear();
PrintHeader(nest, plate, drawing, existingCount, options);
var (success, elapsed) = Fill(nest, plate, drawing, options);
var overlapCount = CheckOverlaps(plate, options);
// Flush and close the log before printing results.
Trace.Flush();
log?.Dispose();
PrintResults(success, plate, elapsed);
Save(nest, options);
return options.CheckOverlaps && overlapCount > 0 ? 1 : 0;
}
static Options ParseArgs(string[] args)
{
var o = new Options();
for (var i = 0; i < args.Length; i++) for (var i = 0; i < args.Length; i++)
{ {
switch (args[i]) switch (args[i])
{ {
case "--drawing" when i + 1 < args.Length: case "--drawing" when i + 1 < args.Length:
drawingName = args[++i]; o.DrawingName = args[++i];
break; break;
case "--plate" when i + 1 < args.Length: case "--plate" when i + 1 < args.Length:
plateIndex = int.Parse(args[++i]); o.PlateIndex = int.Parse(args[++i]);
break; break;
case "--output" when i + 1 < args.Length: case "--output" when i + 1 < args.Length:
outputFile = args[++i]; o.OutputFile = args[++i];
break; break;
case "--quantity" when i + 1 < args.Length: case "--quantity" when i + 1 < args.Length:
quantity = int.Parse(args[++i]); o.Quantity = int.Parse(args[++i]);
break; break;
case "--spacing" when i + 1 < args.Length: case "--spacing" when i + 1 < args.Length:
spacing = double.Parse(args[++i]); o.Spacing = double.Parse(args[++i]);
break; break;
case "--size" when i + 1 < args.Length: case "--size" when i + 1 < args.Length:
var parts = args[++i].Split('x'); var parts = args[++i].Split('x');
if (parts.Length == 2) if (parts.Length == 2)
{ {
plateWidth = double.Parse(parts[0]); o.PlateWidth = double.Parse(parts[0]);
plateHeight = double.Parse(parts[1]); o.PlateHeight = double.Parse(parts[1]);
} }
break; break;
case "--check-overlaps": case "--check-overlaps":
checkOverlaps = true; o.CheckOverlaps = true;
break; break;
case "--no-save": case "--no-save":
noSave = true; o.NoSave = true;
break; break;
case "--no-log": case "--no-log":
noLog = true; o.NoLog = true;
break; break;
case "--keep-parts": case "--keep-parts":
keepParts = true; o.KeepParts = true;
break; break;
case "--template" when i + 1 < args.Length: case "--template" when i + 1 < args.Length:
templateFile = args[++i]; o.TemplateFile = args[++i];
break; break;
case "--autonest": case "--autonest":
autoNest = true; o.AutoNest = true;
break; break;
case "--help": case "--help":
case "-h": case "-h":
PrintUsage(); PrintUsage();
return 0; return null;
default: default:
if (!args[i].StartsWith("--") && nestFile == null) if (!args[i].StartsWith("--"))
nestFile = args[i]; o.InputFiles.Add(args[i]);
break; break;
} }
} }
if (string.IsNullOrEmpty(nestFile) || !File.Exists(nestFile)) return o;
{
PrintUsage();
return 1;
} }
// Set up debug log file. static StreamWriter SetUpLog(Options options)
StreamWriter logWriter = null;
if (!noLog)
{ {
var logDir = Path.Combine(Path.GetDirectoryName(nestFile), "test-harness-logs"); if (options.NoLog)
return null;
var baseDir = Path.GetDirectoryName(options.InputFiles[0]);
var logDir = Path.Combine(baseDir, "test-harness-logs");
Directory.CreateDirectory(logDir); Directory.CreateDirectory(logDir);
var logFile = Path.Combine(logDir, $"debug-{DateTime.Now:yyyyMMdd-HHmmss}.log"); var logFile = Path.Combine(logDir, $"debug-{DateTime.Now:yyyyMMdd-HHmmss}.log");
logWriter = new StreamWriter(logFile) { AutoFlush = true }; var writer = new StreamWriter(logFile) { AutoFlush = true };
Trace.Listeners.Add(new TextWriterTraceListener(logWriter)); Trace.Listeners.Add(new TextWriterTraceListener(writer));
Console.WriteLine($"Debug log: {logFile}"); Console.WriteLine($"Debug log: {logFile}");
return writer;
} }
// Load nest. static Nest LoadOrCreateNest(Options options)
var reader = new NestReader(nestFile); {
var nest = reader.Read(); var nestFile = options.InputFiles.FirstOrDefault(f =>
f.EndsWith(".zip", StringComparison.OrdinalIgnoreCase));
var dxfFiles = options.InputFiles.Where(f =>
f.EndsWith(".dxf", StringComparison.OrdinalIgnoreCase)).ToList();
// If we have a nest file, load it and optionally add DXFs.
if (nestFile != null)
{
var nest = new NestReader(nestFile).Read();
if (nest.Plates.Count == 0) if (nest.Plates.Count == 0)
{ {
Console.Error.WriteLine("Error: nest file contains no plates"); Console.Error.WriteLine("Error: nest file contains no plates");
return 1; return null;
} }
if (plateIndex >= nest.Plates.Count) if (options.PlateIndex >= nest.Plates.Count)
{ {
Console.Error.WriteLine($"Error: plate index {plateIndex} out of range (0-{nest.Plates.Count - 1})"); Console.Error.WriteLine($"Error: plate index {options.PlateIndex} out of range (0-{nest.Plates.Count - 1})");
return 1; return null;
} }
var plate = nest.Plates[plateIndex]; foreach (var dxf in dxfFiles)
{
var drawing = ImportDxf(dxf);
// Apply template defaults. if (drawing == null)
if (templateFile != null) return null;
{
if (!File.Exists(templateFile)) nest.Drawings.Add(drawing);
{ Console.WriteLine($"Imported: {drawing.Name}");
Console.Error.WriteLine($"Error: Template not found: {templateFile}");
return 1;
} }
var templateNest = new NestReader(templateFile).Read();
var templatePlate = templateNest.PlateDefaults.CreateNew(); return nest;
}
// DXF-only mode: create a fresh nest.
if (dxfFiles.Count == 0)
{
Console.Error.WriteLine("Error: no nest (.zip) or DXF (.dxf) files specified");
return null;
}
if (!options.PlateWidth.HasValue || !options.PlateHeight.HasValue)
{
Console.Error.WriteLine("Error: --size WxH is required when importing DXF files without a nest");
return null;
}
var newNest = new Nest { Name = "DXF Import" };
var plate = new Plate { Size = new Size(options.PlateWidth.Value, options.PlateHeight.Value) };
newNest.Plates.Add(plate);
foreach (var dxf in dxfFiles)
{
var drawing = ImportDxf(dxf);
if (drawing == null)
return null;
newNest.Drawings.Add(drawing);
Console.WriteLine($"Imported: {drawing.Name}");
}
return newNest;
}
static Drawing ImportDxf(string path)
{
var importer = new DxfImporter();
if (!importer.GetGeometry(path, out var geometry))
{
Console.Error.WriteLine($"Error: failed to read DXF file: {path}");
return null;
}
if (geometry.Count == 0)
{
Console.Error.WriteLine($"Error: no geometry found in DXF file: {path}");
return null;
}
var pgm = ConvertGeometry.ToProgram(geometry);
if (pgm == null)
{
Console.Error.WriteLine($"Error: failed to convert geometry: {path}");
return null;
}
var name = Path.GetFileNameWithoutExtension(path);
return new Drawing(name, pgm);
}
static void ApplyTemplate(Plate plate, Options options)
{
if (options.TemplateFile == null)
return;
if (!File.Exists(options.TemplateFile))
{
Console.Error.WriteLine($"Error: Template not found: {options.TemplateFile}");
return;
}
var templatePlate = new NestReader(options.TemplateFile).Read().PlateDefaults.CreateNew();
plate.Thickness = templatePlate.Thickness; plate.Thickness = templatePlate.Thickness;
plate.Quadrant = templatePlate.Quadrant; plate.Quadrant = templatePlate.Quadrant;
plate.Material = templatePlate.Material; plate.Material = templatePlate.Material;
plate.EdgeSpacing = templatePlate.EdgeSpacing; plate.EdgeSpacing = templatePlate.EdgeSpacing;
plate.PartSpacing = templatePlate.PartSpacing; plate.PartSpacing = templatePlate.PartSpacing;
Console.WriteLine($"Template: {templateFile}"); Console.WriteLine($"Template: {options.TemplateFile}");
} }
// Apply overrides. static void ApplyOverrides(Plate plate, Options options)
if (spacing.HasValue) {
plate.PartSpacing = spacing.Value; if (options.Spacing.HasValue)
plate.PartSpacing = options.Spacing.Value;
if (plateWidth.HasValue && plateHeight.HasValue) // Only apply size override when it wasn't already used to create the plate.
plate.Size = new Size(plateWidth.Value, plateHeight.Value); var hasDxfOnly = !options.InputFiles.Any(f => f.EndsWith(".zip", StringComparison.OrdinalIgnoreCase));
// Find drawing. if (options.PlateWidth.HasValue && options.PlateHeight.HasValue && !hasDxfOnly)
var drawing = drawingName != null plate.Size = new Size(options.PlateWidth.Value, options.PlateHeight.Value);
? nest.Drawings.FirstOrDefault(d => d.Name == drawingName) }
static Drawing ResolveDrawing(Nest nest, Options options)
{
var drawing = options.DrawingName != null
? nest.Drawings.FirstOrDefault(d => d.Name == options.DrawingName)
: nest.Drawings.FirstOrDefault(); : nest.Drawings.FirstOrDefault();
if (drawing == null) if (drawing != null)
{ return drawing;
Console.Error.WriteLine(drawingName != null
? $"Error: drawing '{drawingName}' not found. Available: {string.Join(", ", nest.Drawings.Select(d => d.Name))}" Console.Error.WriteLine(options.DrawingName != null
? $"Error: drawing '{options.DrawingName}' not found. Available: {string.Join(", ", nest.Drawings.Select(d => d.Name))}"
: "Error: nest file contains no drawings"); : "Error: nest file contains no drawings");
return 1;
return null;
} }
// Clear existing parts. static void PrintHeader(Nest nest, Plate plate, Drawing drawing, int existingCount, Options options)
var existingCount = plate.Parts.Count; {
if (!keepParts)
plate.Parts.Clear();
Console.WriteLine($"Nest: {nest.Name}"); Console.WriteLine($"Nest: {nest.Name}");
Console.WriteLine($"Plate: {plateIndex} ({plate.Size.Width:F1} x {plate.Size.Length:F1}), spacing={plate.PartSpacing:F2}"); Console.WriteLine($"Plate: {options.PlateIndex} ({plate.Size.Width:F1} x {plate.Size.Length:F1}), spacing={plate.PartSpacing:F2}");
Console.WriteLine($"Drawing: {drawing.Name}"); Console.WriteLine($"Drawing: {drawing.Name}");
Console.WriteLine(options.KeepParts
if (!keepParts) ? $"Keeping {existingCount} existing parts"
Console.WriteLine($"Cleared {existingCount} existing parts"); : $"Cleared {existingCount} existing parts");
else
Console.WriteLine($"Keeping {existingCount} existing parts");
Console.WriteLine("---"); Console.WriteLine("---");
}
// Run fill or autonest. static (bool success, long elapsedMs) Fill(Nest nest, Plate plate, Drawing drawing, Options options)
{
var sw = Stopwatch.StartNew(); var sw = Stopwatch.StartNew();
bool success; bool success;
if (autoNest) if (options.AutoNest)
{ {
// AutoNest: use all drawings (or specific drawing if --drawing given).
var nestItems = new List<NestItem>(); var nestItems = new List<NestItem>();
var qty = options.Quantity > 0 ? options.Quantity : 1;
if (drawingName != null) if (options.DrawingName != null)
{ {
nestItems.Add(new NestItem { Drawing = drawing, Quantity = quantity > 0 ? quantity : 1 }); nestItems.Add(new NestItem { Drawing = drawing, Quantity = qty });
} }
else else
{ {
foreach (var d in nest.Drawings) foreach (var d in nest.Drawings)
nestItems.Add(new NestItem { Drawing = d, Quantity = quantity > 0 ? quantity : 1 }); nestItems.Add(new NestItem { Drawing = d, Quantity = qty });
} }
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");
@@ -199,67 +334,67 @@ if (autoNest)
else else
{ {
var engine = new NestEngine(plate); var engine = new NestEngine(plate);
var item = new NestItem { Drawing = drawing, Quantity = quantity }; var item = new NestItem { Drawing = drawing, Quantity = options.Quantity };
success = engine.Fill(item); success = engine.Fill(item);
} }
sw.Stop(); sw.Stop();
return (success, sw.ElapsedMilliseconds);
// Check overlaps.
var overlapCount = 0;
if (checkOverlaps && plate.Parts.Count > 0)
{
List<Vector> overlapPts;
var hasOverlaps = plate.HasOverlappingParts(out overlapPts);
overlapCount = overlapPts.Count;
if (hasOverlaps)
Console.WriteLine($"OVERLAPS DETECTED: {overlapCount} intersection points");
else
Console.WriteLine("Overlap check: PASS");
} }
// Flush and close the log. static int CheckOverlaps(Plate plate, Options options)
Trace.Flush(); {
logWriter?.Dispose(); if (!options.CheckOverlaps || plate.Parts.Count == 0)
return 0;
// Print results. var hasOverlaps = plate.HasOverlappingParts(out var overlapPts);
Console.WriteLine(hasOverlaps
? $"OVERLAPS DETECTED: {overlapPts.Count} intersection points"
: "Overlap check: PASS");
return overlapPts.Count;
}
static void PrintResults(bool success, Plate plate, long elapsedMs)
{
Console.WriteLine($"Result: {(success ? "success" : "failed")}"); Console.WriteLine($"Result: {(success ? "success" : "failed")}");
Console.WriteLine($"Parts placed: {plate.Parts.Count}"); Console.WriteLine($"Parts placed: {plate.Parts.Count}");
Console.WriteLine($"Utilization: {plate.Utilization():P1}"); Console.WriteLine($"Utilization: {plate.Utilization():P1}");
Console.WriteLine($"Time: {sw.ElapsedMilliseconds}ms"); Console.WriteLine($"Time: {elapsedMs}ms");
// Save output.
if (!noSave)
{
if (outputFile == null)
{
var dir = Path.GetDirectoryName(nestFile);
var name = Path.GetFileNameWithoutExtension(nestFile);
outputFile = Path.Combine(dir, $"{name}-result.zip");
} }
var writer = new NestWriter(nest); static void Save(Nest nest, Options options)
writer.Write(outputFile); {
if (options.NoSave)
return;
var firstInput = options.InputFiles[0];
var outputFile = options.OutputFile ?? Path.Combine(
Path.GetDirectoryName(firstInput),
$"{Path.GetFileNameWithoutExtension(firstInput)}-result.zip");
new NestWriter(nest).Write(outputFile);
Console.WriteLine($"Saved: {outputFile}"); Console.WriteLine($"Saved: {outputFile}");
} }
return checkOverlaps && overlapCount > 0 ? 1 : 0; static void PrintUsage()
void PrintUsage()
{ {
Console.Error.WriteLine("Usage: OpenNest.Console <nest-file> [options]"); Console.Error.WriteLine("Usage: OpenNest.Console <input-files...> [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(" input-files One or more .zip nest files or .dxf drawing files");
Console.Error.WriteLine();
Console.Error.WriteLine("Modes:");
Console.Error.WriteLine(" <nest.zip> Load nest and fill (existing behavior)");
Console.Error.WriteLine(" <part.dxf> --size WxH Import DXF, create plate, and fill");
Console.Error.WriteLine(" <nest.zip> <part.dxf> Load nest and add imported DXF drawings");
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.Error.WriteLine(" --plate <index> Plate index to fill (default: 0)"); Console.Error.WriteLine(" --plate <index> Plate index to fill (default: 0)");
Console.Error.WriteLine(" --quantity <n> Max parts to place (default: 0 = unlimited)"); Console.Error.WriteLine(" --quantity <n> Max parts to place (default: 0 = unlimited)");
Console.Error.WriteLine(" --spacing <value> Override part spacing"); Console.Error.WriteLine(" --spacing <value> Override part spacing");
Console.Error.WriteLine(" --size <WxH> Override plate size (e.g. 120x60)"); Console.Error.WriteLine(" --size <WxH> Override plate size (e.g. 120x60); required for DXF-only mode");
Console.Error.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.Error.WriteLine(" --template <path> Nest template for plate defaults (thickness, quadrant, material, spacing)"); Console.Error.WriteLine(" --template <path> 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(" --autonest Use NFP-based mixed-part autonesting instead of linear fill");
@@ -269,3 +404,22 @@ void PrintUsage()
Console.Error.WriteLine(" --no-log Skip writing debug log file"); Console.Error.WriteLine(" --no-log Skip writing debug log file");
Console.Error.WriteLine(" -h, --help Show this help"); Console.Error.WriteLine(" -h, --help Show this help");
} }
class Options
{
public List<string> InputFiles = new();
public string DrawingName;
public int PlateIndex;
public string OutputFile;
public int Quantity;
public double? Spacing;
public double? PlateWidth;
public double? PlateHeight;
public bool CheckOverlaps;
public bool NoSave;
public bool NoLog;
public bool KeepParts;
public bool AutoNest;
public string TemplateFile;
}
}