using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Threading; using OpenNest; using OpenNest.Diagnostics; using OpenNest.Engine; using OpenNest.Engine.BestFit; using OpenNest.Engine.Jobs; using OpenNest.Engine.Jobs.Placement; using OpenNest.Geometry; using OpenNest.IO; using OpenNest.IO.Bending; return NestConsole.Run(args); static class NestConsole { public static int Run(string[] args) { using var cancellation = new CancellationTokenSource(); ConsoleCancelEventHandler cancel = (_, e) => { e.Cancel = true; cancellation.Cancel(); }; Console.CancelKeyPress += cancel; try { return RunCore(args, cancellation.Token); } catch (OperationCanceledException) { Console.Error.WriteLine("Nesting cancelled; nothing saved."); return 2; } catch (Exception ex) { Console.Error.WriteLine($"Error: {ex.Message}"); return 1; } finally { Console.CancelKeyPress -= cancel; } } static int RunCore(string[] args, CancellationToken token) { token.ThrowIfCancellationRequested(); NestingEngineRegistry.LoadPlugins(Path.Combine(AppContext.BaseDirectory, "Engines")); var options = ParseArgs(args); if (options == null) return 0; // --help was requested if ( options.RepairBendsMillimeters.HasValue && ( options.CadUnits == BendRepairUnits.Unspecified || !double.IsFinite(options.RepairBendsMillimeters.Value) || options.RepairBendsMillimeters <= 0.001 || options.RepairBendsMillimeters > 3.175 ) ) { Console.Error.WriteLine( "Error: --repair-bends-mm requires a limit > 0.001 and <= 3.175 mm and --cad-units inches|mm." ); return 1; } if (options.ListPosts) { ListPostProcessors(options); return 0; } if (!options.AutoNest && (options.MaxSheets != 1 || options.Stock != null)) { Console.Error.WriteLine("Error: --max-sheets and --stock require --autonest."); return 1; } if (options.MaxSheets < 1 || options.Stock is { Count: 0 }) { Console.Error.WriteLine( "Error: --max-sheets needs a whole number >= 1 and --stock needs WxL[:cost] sizes separated by commas (for example 60x120,48x96:85)." ); return 1; } // Validate --engine up front: autonest names a jobs engine, plain fill names a // single-plate placement strategy. Unknown names exit with the valid choices. if (options.AutoNest) { // ResolveName also accepts renamed engines' old names (for example Opus55NestingEngine). if (NestingEngineRegistry.ResolveName(options.Engine) == null) { Console.Error.WriteLine( $"Error: unknown engine '{options.Engine}'. Jobs engines: {string.Join(", ", NestingEngineRegistry.AvailableEngines.Select(e => e.Name))}" ); return 1; } } else { var engine = EngineNameResolver.Classify(options.Engine); if (!engine.IsFillStrategy) { Console.Error.WriteLine( engine.IsJobEngine ? $"Error: engine '{engine.Name}' is a whole-job engine; single-plate fill supports: {string.Join(", ", PlateFillService.BuiltInStrategies)}. Use --autonest for whole-job engines." : $"Error: unknown engine '{engine.Name}'. Fill strategies: {string.Join(", ", PlateFillService.BuiltInStrategies)} (jobs engines such as StockLadder require --autonest)" ); return 1; } } if (options.InputFiles.Count == 0) { PrintUsage(); return 1; } foreach (var f in options.InputFiles) { if (!File.Exists(f)) { Console.Error.WriteLine($"Error: file not found: {f}"); return 1; } } var nest = LoadOrCreateNest(options); if (nest == null) return 1; var plate = nest.Plates[options.PlateIndex]; if (options.AutoNest && options.KeepParts && plate.Parts.Count > 0) { Console.Error.WriteLine("Error: --autonest --keep-parts cannot use an occupied plate. Use plain fill for existing obstacles."); return 2; } ApplyTemplate(plate, options); ApplyOverrides(plate, options); var drawing = ResolveDrawing(nest, options); if (drawing == null) return 1; var existingCount = plate.Parts.Count; if (!options.AutoNest && !options.KeepParts) plate.Parts.Clear(); PrintHeader(nest, plate, drawing, existingCount, options); var (success, elapsed, accepted, plates) = Fill(nest, plate, drawing, options, token); if (!accepted) return 2; // No save or post may run after a rejected proposal. var overlapCount = CheckOverlaps(nest, plates, options); PrintResults(success, nest, plates, elapsed); token.ThrowIfCancellationRequested(); if (!SaveAndPost(nest, options)) return 1; return options.CheckOverlaps && overlapCount > 0 ? 1 : 0; } static Options ParseArgs(string[] args) { var o = new Options(); for (var i = 0; i < args.Length; i++) { switch (args[i]) { case "--repair-bends-mm": o.RepairBendsMillimeters = i + 1 < args.Length && double.TryParse( args[++i], NumberStyles.Float, CultureInfo.InvariantCulture, out var limit ) ? limit : double.NaN; break; case "--cad-units" when i + 1 < args.Length: o.CadUnits = args[++i] switch { "inches" => BendRepairUnits.Inches, "mm" => BendRepairUnits.Millimeters, _ => BendRepairUnits.Unspecified, }; break; case "--drawing" when i + 1 < args.Length: o.DrawingName = args[++i]; break; case "--plate" when i + 1 < args.Length: o.PlateIndex = int.Parse(args[++i]); break; case "--output" when i + 1 < args.Length: o.OutputFile = args[++i]; break; case "--quantity" when i + 1 < args.Length: o.Quantity = int.Parse(args[++i]); break; case "--spacing" when i + 1 < args.Length: o.Spacing = double.Parse(args[++i]); break; case "--size" when i + 1 < args.Length: if (Size.TryParse(args[++i], out var sz)) o.PlateSize = sz; break; case "--check-overlaps": o.CheckOverlaps = true; break; case "--no-save": o.NoSave = true; break; case "--keep-parts": o.KeepParts = true; break; case "--template" when i + 1 < args.Length: o.TemplateFile = args[++i]; break; case "--allow-invalid": o.AllowInvalid = true; break; case "--autonest": o.AutoNest = true; break; case "--max-sheets" when i + 1 < args.Length: o.MaxSheets = int.TryParse(args[++i], NumberStyles.Integer, CultureInfo.InvariantCulture, out var sheets) ? sheets : 0; break; case "--stock" when i + 1 < args.Length: o.Stock = ParseStock(args[++i]); break; case "--engine" when i + 1 < args.Length: o.Engine = args[++i]; break; case "--post" when i + 1 < args.Length: o.PostName = args[++i]; break; case "--acknowledge-post-risks": o.AcknowledgePostRisks = true; break; case "--post-output" when i + 1 < args.Length: o.PostOutput = args[++i]; break; case "--posts-dir" when i + 1 < args.Length: o.PostsDir = args[++i]; break; case "--list-posts": o.ListPosts = true; break; case "--help": case "-h": PrintUsage(); return null; default: if (!args[i].StartsWith("--")) o.InputFiles.Add(args[i]); break; } } return o; } /// Parses "WxL[:cost],..."; any malformed entry returns an empty list, which validation rejects. static List ParseStock(string value) { var stock = new List(); foreach (var entry in value.Split(',', StringSplitOptions.TrimEntries)) { var parts = entry.Split(':', StringSplitOptions.TrimEntries); var cost = 0.0; if ( parts.Length > 2 || !Size.TryParse(parts[0], out var size) || !double.IsFinite(size.Width) || !double.IsFinite(size.Length) || size.Width <= 0 || size.Length <= 0 || ( parts.Length == 2 && ( !double.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out cost) || !double.IsFinite(cost) || cost <= 0 ) ) ) return new List(); stock.Add(new PlateOption { Width = size.Width, Length = size.Length, Cost = cost }); } return stock; } static Nest LoadOrCreateNest(Options options) { var nestFile = options.InputFiles.FirstOrDefault(f => f.EndsWith(NestFormat.FileExtension, StringComparison.OrdinalIgnoreCase) || f.EndsWith(".zip", StringComparison.OrdinalIgnoreCase) ); var dxfFiles = options .InputFiles.Where(f => f.EndsWith(".dxf", StringComparison.OrdinalIgnoreCase) || f.EndsWith(".dwg", StringComparison.OrdinalIgnoreCase) ) .ToList(); // If we have a nest file, load it and optionally add DXFs. if (nestFile != null) { var nest = new NestReader(nestFile) { BestFitStore = BestFitCacheStore.Shared }.Read(); if (nest.Plates.Count == 0) { Console.Error.WriteLine("Error: nest file contains no plates"); return null; } if (options.PlateIndex >= nest.Plates.Count) { Console.Error.WriteLine( $"Error: plate index {options.PlateIndex} out of range (0-{nest.Plates.Count - 1})" ); return null; } foreach (var dxf in dxfFiles) { var drawing = ImportDxf(dxf, options); if (drawing == null) return null; nest.Drawings.Add(drawing); Console.WriteLine($"Imported: {drawing.Name}"); } return nest; } // DXF-only mode: create a fresh nest. if (dxfFiles.Count == 0) { Console.Error.WriteLine("Error: no nest (.nest) or CAD (.dxf/.dwg) files specified"); return null; } if (!options.PlateSize.HasValue) { Console.Error.WriteLine( "Error: --size WxL is required when importing DXF files without a nest" ); return null; } var newNest = new Nest { Name = "DXF Import" }; var plate = new Plate { Size = options.PlateSize.Value }; newNest.Plates.Add(plate); foreach (var dxf in dxfFiles) { var drawing = ImportDxf(dxf, options); if (drawing == null) return null; newNest.Drawings.Add(drawing); Console.WriteLine($"Imported: {drawing.Name}"); } return newNest; } static Drawing ImportDxf(string path, Options options) { try { var result = CadImporter.Import( path, new CadImportOptions { BendRepair = options.RepairBendsMillimeters.HasValue ? new BendRepairOptions { DrawingUnits = options.CadUnits, MaxEndpointMovementMillimeters = options.RepairBendsMillimeters.Value, } : null, } ); foreach (var report in result.BendRepairReports) Console.WriteLine( $"Bend repair {Path.GetFileName(path)} #{report.BendIndex + 1}: {report.Status}: {report.Reason} ({report.OriginalStart} -> {report.Start}; {report.OriginalEnd} -> {report.End})" ); return CadImporter.BuildDrawing(result, result.Entities, result.Bends, 1, null, null); } catch (System.Exception ex) { Console.Error.WriteLine($"Error: failed to import DXF '{path}': {ex.Message}"); return null; } } 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 templateNest = new NestReader(options.TemplateFile) { BestFitStore = BestFitCacheStore.Shared }.Read(); var templatePlate = templateNest.PlateDefaults.CreateNew(); plate.Quadrant = templatePlate.Quadrant; plate.EdgeSpacing = templatePlate.EdgeSpacing; plate.PartSpacing = templatePlate.PartSpacing; Console.WriteLine($"Template: {options.TemplateFile}"); } static void ApplyOverrides(Plate plate, Options options) { if (options.Spacing.HasValue) plate.PartSpacing = options.Spacing.Value; // Only apply size override when it wasn't already used to create the plate. var hasDxfOnly = !options.InputFiles.Any(f => f.EndsWith(NestFormat.FileExtension, StringComparison.OrdinalIgnoreCase) || f.EndsWith(".zip", StringComparison.OrdinalIgnoreCase) ); if (options.PlateSize.HasValue && !hasDxfOnly) plate.Size = options.PlateSize.Value; } static Drawing ResolveDrawing(Nest nest, Options options) { var drawing = options.DrawingName != null ? nest.Drawings.FirstOrDefault(d => d.Name == options.DrawingName) : nest.Drawings.FirstOrDefault(); if (drawing != null) return drawing; 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" ); return null; } static void PrintHeader( Nest nest, Plate plate, Drawing drawing, int existingCount, Options options ) { Console.WriteLine($"Nest: {nest.Name}"); var wa = plate.WorkArea(); Console.WriteLine( $"Plate: {options.PlateIndex} ({plate.Size.Width:F1} x {plate.Size.Length:F1}), spacing={plate.PartSpacing:F2}, edge=({plate.EdgeSpacing.Left},{plate.EdgeSpacing.Bottom},{plate.EdgeSpacing.Right},{plate.EdgeSpacing.Top}), workArea={wa.Width:F1}x{wa.Length:F1}" ); var existingPartsMessage = options.KeepParts ? $"Keeping {existingCount} existing parts" : options.AutoNest ? $"Will replace {existingCount} existing parts only after acceptance" : $"Cleared {existingCount} existing parts"; Console.WriteLine( $""" Drawing: {drawing.Name} {existingPartsMessage} --- """ ); } static (bool success, long elapsedMs, bool accepted, IReadOnlyList plates) Fill( Nest nest, Plate plate, Drawing drawing, Options options, CancellationToken token ) { var sw = Stopwatch.StartNew(); bool success; var accepted = true; IReadOnlyList plates = new[] { plate }; if (options.AutoNest) { var nestItems = new List(); var qty = options.Quantity > 0 ? options.Quantity : 1; if (options.DrawingName != null) { nestItems.Add(new NestItem { Drawing = drawing, Quantity = qty }); } else { foreach (var d in nest.Drawings) nestItems.Add(new NestItem { Drawing = d, Quantity = qty }); } Console.WriteLine( $"AutoNest: {nestItems.Count} drawing(s), {nestItems.Sum(i => i.Quantity)} total parts" ); var outcome = AutoNestJob(nest, plate, nestItems, options, token); accepted = outcome.accepted; success = outcome.committed > 0; if (accepted) plates = outcome.plates; } else { // Single-plate fill: explicit placement strategy through the public service; // the process-global engine registry is never consulted. var strategy = EngineNameResolver.Classify(options.Engine).FillStrategy; var item = new NestItem { Drawing = drawing, Quantity = options.Quantity }; var parts = PlateFillService.FillItem( strategy, plate, item, plate.WorkArea(), null, token ); token.ThrowIfCancellationRequested(); if (parts.Count > 0) plate.Parts.AddRange(parts); success = parts.Count > 0; } sw.Stop(); return (success, sw.ElapsedMilliseconds, accepted, plates); } static (bool accepted, int committed, IReadOnlyList plates) AutoNestJob( Nest nest, Plate plate, List nestItems, Options options, CancellationToken token) { var rejected = (false, 0, (IReadOnlyList)Array.Empty()); // One sheet of the target's size keeps the original single-sheet stock exactly; // otherwise the target is the template for every offered size, unlimited per size // and capped by --max-sheets physical sheets in total. var singleSheet = options.MaxSheets == 1 && options.Stock == null; var stock = singleSheet ? NestStockBuilder.SinglePlate(plate) : NestStockBuilder.FromTemplate(plate, options.Stock); var result = NestPipeline.Run(new NestPipelineRequest( options.Engine, nestItems, stock, singleSheet ? null : new NestJobOptions(maxPlates: options.MaxSheets)), token: token); foreach (var violation in result.Violations) Console.Error.WriteLine($"Violation: {violation}"); foreach (var unplaced in result.Unplaced) Console.Error.WriteLine($"Unplaced: {unplaced.Describe()}"); var tooManySheets = result.Raw.Plates.Count > options.MaxSheets; if (!result.CanKeep || tooManySheets || (!result.IsValid && !options.AllowInvalid)) { Console.Error.WriteLine(tooManySheets ? $"Error: result uses {result.Raw.Plates.Count} sheets, more than --max-sheets {options.MaxSheets}, even with --allow-invalid. Nothing saved." : !result.CanKeep ? "Error: result cannot be represented faithfully, even with --allow-invalid. Nothing saved." : "Error: invalid result discarded. Use --allow-invalid to explicitly keep its violations. Nothing saved."); return rejected; } token.ThrowIfCancellationRequested(); var committed = result.Plates.Sum(p => p.Parts.Count * p.Quantity); if (committed == 0) { Console.Error.WriteLine("Error: no placements returned. Existing layout and output left unchanged."); return rejected; } var costs = result.IsValid && options.Stock != null ? NestCostSummary.FromAccepted(result) : null; // The first layout replaces the target's parts; further layouts become new sheets // right after it, carrying the target's cutting settings. Other plates are unchanged. var insertAt = nest.Plates.IndexOf(plate) + 1; var targetUsed = false; var applied = NestPipelineCommit.ApplyToPlates(result, () => { if (!targetUsed) { targetUsed = true; plate.Parts.Clear(); return plate; } var created = new Plate(plate.Size) { GrainAngle = plate.GrainAngle, CuttingParameters = plate.CuttingParameters, }; nest.Plates.Insert(insertAt++, created); return created; }, options.AllowInvalid, token); Console.WriteLine($"Engine: {options.Engine} — committed {committed} placements on {applied.Sum(p => p.Quantity)} sheet(s)"); if (costs != null) Console.WriteLine($"Cost ({costs.Basis}): gross {costs.GrossTotal:F2}, salvage credit {costs.SalvageCredit:F2}, net {costs.NetScore:F2}"); return (true, committed, applied); } static int CheckOverlaps(Nest nest, IReadOnlyList plates, Options options) { if (!options.CheckOverlaps) return 0; var total = 0; foreach (var plate in plates.Where(p => p.Parts.Count > 0)) { var hasOverlaps = plate.HasOverlappingParts(out var overlapPts); var label = plates.Count > 1 ? $"Plate {nest.Plates.IndexOf(plate)}: " : ""; Console.WriteLine( hasOverlaps ? $"{label}OVERLAPS DETECTED: {overlapPts.Count} intersection points" : $"{label}Overlap check: PASS" ); total += overlapPts.Count; } return total; } static void PrintResults(bool success, Nest nest, IReadOnlyList plates, long elapsedMs) { if (plates.Count > 1) { foreach (var plate in plates) Console.WriteLine( $"Plate {nest.Plates.IndexOf(plate)}: {plate.Size.Width:F1} x {plate.Size.Length:F1} x{plate.Quantity}, {plate.Parts.Count} parts, utilization {plate.Utilization():P1}" ); } var area = plates.Sum(p => p.Size.Width * p.Size.Length * p.Quantity); var utilization = area > 0 ? plates.Sum(p => p.Utilization() * p.Size.Width * p.Size.Length * p.Quantity) / area : 0; Console.WriteLine( $""" Result: {(success ? "success" : "failed")} Parts placed: {plates.Sum(p => p.Parts.Count * p.Quantity)} Utilization: {utilization:P1} Time: {elapsedMs}ms """ ); } static void Save(Nest nest, Options options) { if (options.NoSave) return; var outputFile = NestOutputPath(options); new NestWriter(nest) { BestFitStore = BestFitCacheStore.Shared }.Write(outputFile); Console.WriteLine($"Saved: {outputFile}"); } static string NestOutputPath(Options options) => options.OutputFile ?? Path.Combine(Path.GetDirectoryName(options.InputFiles[0]), $"{Path.GetFileNameWithoutExtension(options.InputFiles[0])}-result{NestFormat.FileExtension}"); static string ResolvePostsDir(Options options) { if (options.PostsDir != null) return options.PostsDir; var exePath = Assembly.GetEntryAssembly()?.Location ?? typeof(NestConsole).Assembly.Location; return Path.Combine(Path.GetDirectoryName(exePath), "Posts"); } static List LoadPostProcessors(string postsDir) { var processors = new List(); if (!Directory.Exists(postsDir)) return processors; foreach (var file in Directory.GetFiles(postsDir, "*.dll")) { try { var assembly = Assembly.LoadFrom(file); foreach (var type in assembly.GetTypes()) { if ( !typeof(IPostProcessor).IsAssignableFrom(type) || type.IsInterface || type.IsAbstract ) continue; if (Activator.CreateInstance(type) is IPostProcessor processor) processors.Add(processor); } } catch (Exception ex) { Console.Error.WriteLine( $"Warning: failed to load post processor from {Path.GetFileName(file)}: {ex.Message}" ); } } return processors; } static void ListPostProcessors(Options options) { var postsDir = ResolvePostsDir(options); var processors = LoadPostProcessors(postsDir); if (processors.Count == 0) { Console.WriteLine($"No post processors found in: {postsDir}"); return; } Console.WriteLine($"Post processors ({postsDir}):"); foreach (var p in processors) Console.WriteLine($" {p.Name,-30} {p.Description}"); } static bool SaveAndPost(Nest nest, Options options) { if (options.PostName == null) { Save(nest, options); return true; } var postsDir = ResolvePostsDir(options); var processors = LoadPostProcessors(postsDir); var post = processors.FirstOrDefault(p => p.Name.Equals(options.PostName, StringComparison.OrdinalIgnoreCase) ); if (post == null) { Console.Error.WriteLine($"Error: post processor '{options.PostName}' not found"); if (processors.Count > 0) Console.Error.WriteLine( $"Available: {string.Join(", ", processors.Select(p => p.Name))}" ); else Console.Error.WriteLine($"No post processors found in: {postsDir}"); return false; } var verification = PostVerificationAnalyzer.AnalyzeForPost(nest, post); Console.WriteLine(verification.ToDisplayText()); if (!verification.CanPost(options.AcknowledgePostRisks)) { Console.Error.WriteLine("Posting blocked: review these warnings. To accept the risks, including possible head crashes and machine or material damage, explicitly use --acknowledge-post-risks for this invocation."); return false; } var outputFile = options.PostOutput; if (outputFile == null) { var firstInput = options.InputFiles[0]; outputFile = Path.Combine( Path.GetDirectoryName(firstInput), $"{Path.GetFileNameWithoutExtension(firstInput)}.cnc" ); } var outputFiles = post is IMultiFilePostProcessor multiFile ? multiFile.GetOutputFiles(nest, outputFile) : new[] { outputFile }; if (!options.NoSave && outputFiles.Any(file => string.Equals( Path.GetFullPath(file), Path.GetFullPath(NestOutputPath(options)), StringComparison.OrdinalIgnoreCase))) { Console.Error.WriteLine("Error: nest save and CNC output paths must be different. No output was written."); return false; } Save(nest, options); post.Post(nest, outputFile); foreach (var file in outputFiles) Console.WriteLine($"Post: {post.Name} -> {file}"); return true; } static void PrintUsage() { Console.Error.WriteLine( """ Usage: OpenNest.Console [options] Arguments: input-files One or more .nest nest files or .dxf/.dwg drawing files Modes: Load nest and fill (existing behavior) --size WxL Import DXF, create plate, and fill Load nest and add imported DXF drawings Options: --repair-bends-mm Opt-in endpoint/tick repair, limit >0.001 to 3.175 physical mm --cad-units inches|mm Explicit source coordinate units required for bend repair --drawing Drawing name to fill with (default: first drawing) --plate Plate index to fill (default: 0) --quantity Max parts to place (default: 0 = unlimited) --spacing Override part spacing --size Override plate size (e.g. 60x120); required for DXF-only mode --output Output nest file path (default: -result.nest) --template Nest template for plate defaults (thickness, quadrant, material, spacing) --autonest Validated whole-job nesting (one sheet unless --max-sheets); replaces only after acceptance --max-sheets With --autonest: physical sheets the job may use (default: 1); extra layouts are inserted as new plates after the target plate --stock With --autonest: sheet sizes to offer, each unlimited (default: the target plate's size); spacing, quadrant and clamps follow the target --allow-invalid Explicitly keep representable invalid autonest layouts (default: reject, exit 2) --engine With --autonest: jobs engine (default: Default; also Irregular, Rectangles, Fill, ...). Without --autonest: fill strategy (Fill (default), Strip, Vertical Remnant, Horizontal Remnant) --keep-parts Don't clear existing parts before filling --check-overlaps Run overlap detection after fill (exit code 1 if found) --no-save Skip saving output file --post Run a post processor after nesting --post-output Output file for post processor (default: .cnc) --acknowledge-post-risks Explicitly accept displayed verification risks for this invocation --posts-dir Directory containing post processor DLLs (default: Posts/) --list-posts List available post processors and exit -h, --help Show this help """ ); } class Options { public List InputFiles = new(); public string DrawingName; public int PlateIndex; public string OutputFile; public int Quantity; public double? Spacing; public Size? PlateSize; public bool CheckOverlaps; public bool NoSave; public bool KeepParts; public bool AutoNest; public bool AllowInvalid; public int MaxSheets = 1; public List Stock; public string Engine = "Default"; public string TemplateFile; public string PostName; public bool AcknowledgePostRisks; public string PostOutput; public string PostsDir; public bool ListPosts; public double? RepairBendsMillimeters; public BendRepairUnits CadUnits; } }