From d6d819bfed96518f975df59b730eb29585a5c79f Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Wed, 23 Sep 2026 15:15:45 -0400 Subject: [PATCH] chore: remove tools directory One-off helper utilities don't belong in the repo. PepNestExport moved to /home/aj/src/PepNestExport as a standalone companion project; NestDxfJob and StreamGravographJob are removed (recoverable from history). --- README.md | 29 - tools/NestDxfJob/NestDxfJob.csproj | 10 - tools/NestDxfJob/Program.cs | 296 ------- tools/PepNestExport/PepNestExport.csproj | 13 - tools/PepNestExport/Program.cs | 825 ------------------ tools/StreamGravographJob/Program.cs | 343 -------- .../StreamGravographJob.csproj | 14 - 7 files changed, 1530 deletions(-) delete mode 100644 tools/NestDxfJob/NestDxfJob.csproj delete mode 100644 tools/NestDxfJob/Program.cs delete mode 100644 tools/PepNestExport/PepNestExport.csproj delete mode 100644 tools/PepNestExport/Program.cs delete mode 100644 tools/StreamGravographJob/Program.cs delete mode 100644 tools/StreamGravographJob/StreamGravographJob.csproj diff --git a/README.md b/README.md index 1c68826..89f8027 100644 --- a/README.md +++ b/README.md @@ -120,35 +120,6 @@ var domainResult = NestResultMaterializer.Materialize(job, result); **Single-plate placement:** interactive fills use `PlateFillService`, which explicitly selects one of `Default`, `Strip`, `Vertical Remnant`, or `Horizontal Remnant` and returns proposed parts without mutating the caller's plate. The caller remains responsible for preview accept/cancel and attachment. Whole-job work in the desktop app, console, MCP server, and public API resolves a named `INestingEngine`, solves one `NestJob`, and materializes committed results. `NestRunner.RunAsync` reports status, stop reason, part fulfillment, stock usage, and plate-to-stock mapping; `.nestquote` archives carry a schema version and round-trip incomplete jobs. -### Fresh DXF job verification (headless) - -`tools/NestDxfJob` imports a complete quantity workbook and runs a registered whole-job engine. It never reuses saved drawing geometry or placements. The workbook must have a `Parts` worksheet with exactly one `Part Name` and `Qty Required` column; names match DXF filename stems exactly. Invalid/fractional/negative quantities, duplicate names, and missing required DXFs fail explicitly. Zero-demand rows are not imported; additional DXFs with no positive workbook demand are listed and not assigned an invented quantity. - -```bash -dotnet run --project tools/NestDxfJob -- \ - /path/to/dxfs /path/to/parts.xlsx /path/to/settings.nest \ - /path/to/new-results-directory Strip -``` - -The settings nest supplies units, material metadata, per-part rotation constraints/priority where names match, and distinct plate dimensions/clearances/quadrants. Stock is unlimited copies of those settings with a 40-sheet cap, not a claim about physical inventory. DXFs with explicit conflicting units reject; unitless DXFs use the template units without rescaling. `CadImporter` is called with `DetectBends = false`: default DXF filtering removes case-insensitive `ETCH`/`SCRIBE` layers before optimization, and bend detection cannot regenerate marks. - -The output directory must not exist. The tool writes `imported-cut-only.nest` and `import-report.json` (including input hashes, excluded marks, and unmatched DXFs), then runs the selected engine with a ten-minute cancellation budget. A complete result must pass quantity, bounds, overlap/spacing and cut-only checks, then save/reload and pass them again before success. `validation-report.json` records per-part fulfillment and placements. A partial or invalid result exits nonzero and is not published as a successful nest. Use `import-only` instead of an engine name to verify and save only the imported job. This verifies nesting geometry, not machine-ready CNC lead-ins or post-processing. - -### PEP nest export (benchmark against PEP) - -`tools/PepNestExport` converts a year of PEP nests into `.nest` files for `OpenNest.Benchmark`. It lists nests from PepApi (`/nests/{year}`), downloads each `.pep` file (`/nests/{year}/{name}/download`), and reads it with `PepLib.Core` from the sibling `PepApi.Core` repo. Override the path with `-p:PepLibProject=` if that repo is cloned elsewhere. - -```bash -dotnet run --project tools/PepNestExport -c Release -- "/path/to/PEP 2026 nests" --year 2026 -dotnet OpenNest.Benchmark/bin/Release/net8.0/OpenNest.Benchmark.dll "/path/to/PEP 2026 nests" --engines Opus55NestingEngine --csv results.csv -``` - -Each `.nest` keeps PEP's own layout: plate sizes and duplicate counts, part spacing, edge spacing, quadrant and every placement. The benchmark therefore scores PEP as its `Baseline` row and offers engines only the sheet sizes PEP used, unless you pass `--sheet-sizes`. Drawing geometry comes from PEP's loops, flattened with sub-loop (hole) calls continuing the incremental position. Lead-ins, lead-outs, scribe, display and `DESTRUCT CUT` moves are dropped, and uncut micro-joint tabs of 0.25 or less are closed: open cut runs are chained end to start across the tab and bridged with a cut line, but only where they form a closed loop, so separate contours that happen to lie close together are never merged. The tabs themselves are not kept. Skeleton and display-only parts are excluded. - -By default `--quantity nested` sets demand to what PEP actually nested; `--quantity required` uses PEP's required counts instead. Other options: `--nests`, `--status` (default: every status except `Deleted`), `--parallel` and `--force`. - -The tool writes `pep-baseline.csv` (sheets, sheet area, part area and utilization per nest) and a `.violations.txt` when PEP's layout fails validation. PEP places parts at exactly the nominal spacing and rounds coordinates to about 4 decimals, so the strict benchmark validator usually rejects the PEP baseline. Its sheet count and area still show in the report. `RelaxedValid` repeats the check allowing 0.025 on spacing and 0.001 on edges; a failure there means a real overlap or a genuinely tight manual placement. Validation runs on the saved file and is capped at 60 seconds per nest: `NestValidator` can take minutes on parts with hundreds of outline segments and many holes, and those rows report `timeout`. Programs are stored incremental, like CAD-imported drawings, because the desktop renderer only applies part locations to incremental programs. - ### Run ```bash diff --git a/tools/NestDxfJob/NestDxfJob.csproj b/tools/NestDxfJob/NestDxfJob.csproj deleted file mode 100644 index 9418a95..0000000 --- a/tools/NestDxfJob/NestDxfJob.csproj +++ /dev/null @@ -1,10 +0,0 @@ - - - Exe - net8.0 - enable - - - - - diff --git a/tools/NestDxfJob/Program.cs b/tools/NestDxfJob/Program.cs deleted file mode 100644 index 93160aa..0000000 --- a/tools/NestDxfJob/Program.cs +++ /dev/null @@ -1,296 +0,0 @@ -using System.Diagnostics; -using System.Globalization; -using System.Security.Cryptography; -using System.Text.Json; -using OpenNest; -using OpenNest.Benchmark; -using OpenNest.CNC; -using OpenNest.IO; -using OpenNest.IO.Bom; - -CultureInfo.CurrentCulture = CultureInfo.InvariantCulture; -if (args.Length != 5) -{ - Console.Error.WriteLine( - "Usage: NestDxfJob " - ); - return 1; -} - -try -{ - var directory = Path.GetFullPath(args[0]); - var workbookPath = Path.GetFullPath(args[1]); - var templatePath = Path.GetFullPath(args[2]); - var outputDirectory = Path.GetFullPath(args[3]); - if (Directory.Exists(outputDirectory) || File.Exists(outputDirectory)) - throw new IOException( - "Output directory must be new; existing inputs/results are never overwritten." - ); - var engineInfo = - args[4] == "import-only" - ? null - : NestingEngineRegistry.AvailableEngines.SingleOrDefault(e => e.Name == args[4]) - ?? throw new ArgumentException($"Unknown engine: {args[4]}"); - var quantities = PartQuantityReader.Read(workbookPath); - var paths = Directory - .GetFiles(directory) - .Where(p => Path.GetExtension(p).Equals(".dxf", StringComparison.OrdinalIgnoreCase)) - .Order(StringComparer.Ordinal) - .ToArray(); - var files = paths.ToDictionary(Path.GetFileNameWithoutExtension, StringComparer.Ordinal); - var demand = quantities - .Where(p => p.Value > 0) - .ToDictionary(p => p.Key, p => p.Value, StringComparer.Ordinal); - var missing = demand.Keys.Except(files.Keys, StringComparer.Ordinal).ToArray(); - if (missing.Length > 0) - throw new InvalidDataException($"Missing exact-name DXFs: {string.Join(", ", missing)}"); - var unrequested = files.Keys.Except(demand.Keys, StringComparer.Ordinal).ToArray(); - foreach (var name in unrequested) - Console.WriteLine($"Not requested by workbook (not imported): {files[name]}"); - var hashes = paths - .Append(workbookPath) - .Append(templatePath) - .Distinct() - .ToDictionary(p => p, Hash); - var template = new NestReader(templatePath).Read(); - // Saved placements and drawing geometry are NOT imported from the template. - var input = new Nest(template.Name) - { - Units = template.Units, - Material = template.Material, - Thickness = template.Thickness, - Customer = template.Customer, - DateCreated = DateTime.Now, - Notes = - "Fresh DXF import; ETCH/SCRIBE excluded and bend generation disabled. Quantities from workbook. Template supplies stock settings only.", - }; - var stocks = - template.Plates.Count > 0 - ? template.Plates.ToList() - : new List { template.PlateDefaults.CreateNew() }; - var distinctStocks = stocks - .GroupBy(p => new - { - p.Size, - p.Quadrant, - p.PartSpacing, - p.EdgeSpacing.Left, - p.EdgeSpacing.Right, - p.EdgeSpacing.Top, - p.EdgeSpacing.Bottom, - }) - .Select(g => g.First()) - .ToList(); - foreach (var stock in distinctStocks) - { - input.PlateDefaults.SetFromExisting(stock); - input.Plates.Add(input.PlateDefaults.CreateNew()); - } - input.PlateDefaults.SetFromExisting(distinctStocks[0]); - var imports = new List(); - foreach (var (name, quantity) in demand) - { - var import = CadImporter.Import(files[name], new CadImportOptions { DetectBends = false }); - var sourceUnits = (int)import.Document.Header.InsUnits; - var expectedUnits = template.Units == Units.Inches ? 1 : 4; - if (sourceUnits != 0 && sourceUnits != expectedUnits) - throw new InvalidDataException($"DXF units conflict with template: {name}"); - var marks = import.Document.Entities.Count(e => IsMark(e.Layer?.Name)); - if (import.Entities.Any(e => IsMark(e.Layer?.Name)) || import.Bends.Count != 0) - throw new InvalidDataException($"Cut-only import retained markings: {name}"); - var drawing = CadImporter.BuildDrawing( - import, - import.Entities, - import.Bends, - quantity, - template.Customer, - null - ); - drawing.Material = template.Material; - var previous = template.Drawings.SingleOrDefault(d => d.Name == name); - if (previous != null) - { - drawing.Constraints = previous.Constraints; - drawing.Priority = previous.Priority; - } - EnsureCutOnly(drawing); - input.Drawings.Add(drawing); - imports.Add( - new - { - Name = name, - Required = quantity, - RemovedMarks = marks, - SourceUnits = sourceUnits, - CutEntities = import.Entities.Count, - drawing.Area, - } - ); - } - var job = new NestJob( - input.Drawings.Select(d => DrawingJobMapper.FromDrawing(d.Name, d, d.Quantity.Required)), - distinctStocks.Select((p, i) => DrawingJobMapper.FromPlate($"stock-{i + 1}", p, null)), - new NestJobOptions(maxPlates: 40) - ); - NestJobValidator.Validate(job); - VerifySources(hashes); - Directory.CreateDirectory(outputDirectory); - var inputPath = Path.Combine(outputDirectory, "imported-cut-only.nest"); - new NestWriter(input).Write(inputPath); - var jsonOptions = new JsonSerializerOptions { WriteIndented = true }; - File.WriteAllText( - Path.Combine(outputDirectory, "import-report.json"), - JsonSerializer.Serialize( - new - { - Workbook = workbookPath, - Template = templatePath, - Units = template.Units.ToString(), - UnitPolicy = "Explicit DXF units must agree; unitless DXFs use template units without scaling.", - StockPolicy = "Unlimited copies of the template's distinct physical sheet settings; maximum 40 sheets. Not an inventory assertion.", - SourceHashes = hashes, - UnrequestedDxfs = unrequested, - Imports = imports, - Requested = demand.Values.Sum(), - Drawings = demand.Count, - }, - jsonOptions - ) - ); - Console.WriteLine( - $"Imported {demand.Count} drawings / {demand.Values.Sum()} pieces. Input: {inputPath}" - ); - if (engineInfo == null) - return 0; - using var cancellation = new CancellationTokenSource(TimeSpan.FromMinutes(10)); - var timer = Stopwatch.StartNew(); - var result = engineInfo.Factory().Solve(job, new JobProgress(), cancellation.Token); - Console.WriteLine( - $"{result.Status}: {result.Fulfillment.Sum(f => f.Placed)}/{demand.Values.Sum()}, {result.Plates.Count} sheets, {timer.Elapsed.TotalSeconds:F2}s" - ); - var materialized = NestResultMaterializer.Materialize(job, result); - var output = materialized.Nest; - output.Name = input.Name; - output.Units = input.Units; - output.Material = input.Material; - output.Thickness = input.Thickness; - output.Customer = input.Customer; - output.Notes = input.Notes; - output.DateCreated = input.DateCreated; - output.PlateDefaults = input.PlateDefaults; - foreach (var drawing in input.Drawings) - { - var placed = materialized.DrawingsByPartId[drawing.Name]; - placed.Name = drawing.Name; - placed.Color = drawing.Color; - placed.Source = drawing.Source; - placed.SourceEntities = drawing.SourceEntities; - placed.SuppressedEntityIds = drawing.SuppressedEntityIds; - placed.Material = drawing.Material; - placed.Customer = drawing.Customer; - } - var violations = Validate(output, demand); - if (result.Status != NestJobStatus.Complete) - violations.Add($"Incomplete job: {result.StopReason}"); - VerifySources(hashes); - var resultPath = Path.Combine(outputDirectory, $"{input.Name}-{engineInfo.Name}.nest"); - if (violations.Count == 0) - { - new NestWriter(output).Write(resultPath); - var reloaded = new NestReader(resultPath).Read(); - violations.AddRange(Validate(reloaded, demand)); - NestJobValidator.Validate( - new NestJob( - reloaded.Drawings.Select(d => - DrawingJobMapper.FromDrawing(d.Name, d, d.Quantity.Required) - ), - job.Plates - ) - ); - if (violations.Count != 0) - File.Move(resultPath, resultPath + ".invalid"); - } - File.WriteAllText( - Path.Combine(outputDirectory, "validation-report.json"), - JsonSerializer.Serialize( - new - { - Engine = engineInfo.Name, - Status = result.Status.ToString(), - StopReason = result.StopReason.ToString(), - ElapsedSeconds = timer.Elapsed.TotalSeconds, - Requested = demand.Values.Sum(), - Placed = result.Fulfillment.Sum(f => f.Placed), - result.Fulfillment, - result.StockUsage, - result.Plates, - Violations = violations, - SavedNestReloadPassed = violations.Count == 0, - SourceHashesUnchanged = true, - }, - jsonOptions - ) - ); - if (violations.Count != 0) - throw new InvalidDataException(string.Join(Environment.NewLine, violations)); - Console.WriteLine($"Verified complete saved nest: {resultPath}"); - return 0; -} -catch (Exception exception) -{ - Console.Error.WriteLine(exception.ToString()); - return 1; -} - -static bool IsMark(string layer) => - string.Equals(layer, "ETCH", StringComparison.OrdinalIgnoreCase) - || string.Equals(layer, "SCRIBE", StringComparison.OrdinalIgnoreCase); -static string Hash(string path) => Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(path))); -static void VerifySources(Dictionary hashes) -{ - foreach (var (path, hash) in hashes) - if (Hash(path) != hash) - throw new IOException($"Source changed during run: {path}"); -} -static void EnsureCutOnly(Drawing drawing) -{ - if ( - drawing.Program.Codes.Any(c => - c is LinearMove l && l.Layer != LayerType.Cut - || c is ArcMove a && a.Layer != LayerType.Cut - ) - ) - throw new InvalidDataException($"Non-cut motion in {drawing.Name}"); -} -static List Validate(Nest nest, IReadOnlyDictionary demand) -{ - var requirements = nest.Drawings.ToDictionary(d => d, d => (d.Name, demand[d.Name])); - var validation = NestValidator.Validate( - nest.Plates.Select(p => (p, p.Parts.ToList())).ToList(), - requirements - ); - var counts = nest - .Plates.SelectMany(p => p.Parts) - .GroupBy(p => p.BaseDrawing.Name) - .ToDictionary(g => g.Key, g => g.Count()); - foreach (var (name, required) in demand) - if (counts.GetValueOrDefault(name) != required) - validation.Violations.Add( - $"{name}: {counts.GetValueOrDefault(name)} placed, {required} required" - ); - foreach (var drawing in nest.Drawings) - EnsureCutOnly(drawing); - return validation.Violations; -} - -sealed class JobProgress : IProgress -{ - public void Report(NestJobProgress value) - { - if (value.Stage == NestJobStage.PlateCommitted) - Console.WriteLine( - $"Committed {value.CommittedPlates} sheets / {value.CommittedParts} parts" - ); - } -} diff --git a/tools/PepNestExport/PepNestExport.csproj b/tools/PepNestExport/PepNestExport.csproj deleted file mode 100644 index e6efc19..0000000 --- a/tools/PepNestExport/PepNestExport.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - Exe - net8.0 - enable - - $(MSBuildThisFileDirectory)..\..\..\PepApi.Core\PepLib.Core\PepLib.Core.csproj - - - - - - diff --git a/tools/PepNestExport/Program.cs b/tools/PepNestExport/Program.cs deleted file mode 100644 index fb02af8..0000000 --- a/tools/PepNestExport/Program.cs +++ /dev/null @@ -1,825 +0,0 @@ -using System.Collections.Concurrent; -using System.Globalization; -using System.Net.Http.Json; -using System.Text; -using System.Text.Json; -using OpenNest; -using OpenNest.Benchmark; -using OpenNest.CNC; -using OpenNest.Geometry; -using OpenNest.IO; -using PepCodes = PepLib.Codes; -using PepModels = PepLib.Models; -using PepVector = PepLib.Geometry.Vector; - -// Converts PEP nests (downloaded through PepApi, parsed with PepLib) into OpenNest .nest files -// for OpenNest.Benchmark. Each .nest keeps PEP's own layout - sheet sizes, duplicates, part -// spacing, edge spacing, quadrant and every placement - so the benchmark scores PEP as its -// "Baseline" row and offers engines exactly the sheet sizes PEP used. - -CultureInfo.CurrentCulture = CultureInfo.InvariantCulture; -CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture; - -var options = Options.Parse(args); -if (options == null) -{ - Console.Error.WriteLine( - """ - Usage: PepNestExport [options] - - --year PEP year to export (default 2026) - --api PepApi base URL (default http://10.10.100.134:8085) - --nests N1,N2,... Only these nest names (default: every nest in the year) - --status S1,S2,... Only these PEP statuses, e.g. "Has been cut,To be cut" - (default: every status except Deleted) - --quantity nested|required Part demand written to the .nest (default nested): - nested = what PEP actually nested, so the PEP layout is a - valid, fully placed baseline - required = PEP's required qty; where PEP over-nested, the - baseline is flagged over-quantity - --parallel Nests converted at once (default 4) - --force Re-download and re-convert nests that already exist - - Writes /.nest, caches the raw files in /pep/, and writes a - per-nest summary to /pep-baseline.csv. Benchmark the output folder with: - OpenNest.Benchmark --engines Opus55 --csv results.csv - """ - ); - return 1; -} - -Directory.CreateDirectory(options.OutputDirectory); -var pepDirectory = Path.Combine(options.OutputDirectory, "pep"); -Directory.CreateDirectory(pepDirectory); - -using var http = new HttpClient -{ - BaseAddress = new Uri(options.ApiBaseUrl.TrimEnd('/') + "/"), - Timeout = TimeSpan.FromMinutes(2), -}; -var json = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; - -List summaries; -try -{ - summaries = - await http.GetFromJsonAsync>($"nests/{options.Year}", json) - ?? new List(); -} -catch (Exception ex) -{ - Console.Error.WriteLine($"Could not list {options.Year} nests from {http.BaseAddress}: {ex.Message}"); - return 1; -} - -var selected = summaries - .Where(s => options.Nests.Count == 0 || options.Nests.Contains(s.Name)) - .Where(s => - options.Statuses.Count > 0 - ? options.Statuses.Contains(s.Status) - : !string.Equals(s.Status, "Deleted", StringComparison.OrdinalIgnoreCase) - ) - .OrderBy(s => s.Name, StringComparer.OrdinalIgnoreCase) - .ToList(); - -var missingNests = options.Nests.Except(summaries.Select(s => s.Name), StringComparer.OrdinalIgnoreCase); -foreach (var name in missingNests) - Console.Error.WriteLine($"Warning: {name} is not a {options.Year} nest in PepApi."); - -Console.WriteLine( - $"{summaries.Count} nests in {options.Year}; converting {selected.Count} (quantity = {options.Quantity})." -); - -var rows = new ConcurrentBag(); -var completed = 0; - -await Parallel.ForEachAsync( - selected, - new ParallelOptions { MaxDegreeOfParallelism = options.Parallel }, - async (summary, cancellation) => - { - var row = new ReportRow { Nest = summary.Name, PepStatus = summary.Status }; - try - { - var nestPath = Path.Combine(options.OutputDirectory, summary.Name + ".nest"); - if (File.Exists(nestPath) && !options.Force) - { - row.Result = "skipped (exists; --force to redo)"; - } - else - { - var pepPath = Path.Combine(pepDirectory, summary.Name + ".pep"); - if (!File.Exists(pepPath) || options.Force) - { - var url = $"nests/{options.Year}/{Uri.EscapeDataString(summary.Name)}/download"; - var bytes = await http.GetByteArrayAsync(url, cancellation); - await File.WriteAllBytesAsync(pepPath, bytes, cancellation); - } - - PepModels.Nest pep; - using (var stream = File.OpenRead(pepPath)) - pep = PepModels.Nest.Load(stream); - - PepConverter.Convert(summary, pep, options.Quantity, row, nestPath); - } - } - catch (Exception ex) - { - row.Result = "error: " + ex.Message.ReplaceLineEndings(" "); - } - - rows.Add(row); - var done = Interlocked.Increment(ref completed); - Console.WriteLine($"[{done}/{selected.Count}] {row.Nest}: {row.Result}"); - } -); - -var ordered = rows.OrderBy(r => r.Nest, StringComparer.OrdinalIgnoreCase).ToList(); -var csvPath = Path.Combine(options.OutputDirectory, "pep-baseline.csv"); -ReportRow.WriteCsv(csvPath, ordered); - -var converted = ordered.Where(r => r.Result == "ok").ToList(); -Console.WriteLine(); -Console.WriteLine( - $"Converted {converted.Count} (with geometry warnings: {converted.Count(r => r.GeometryWarnings.Count > 0)}; " - + $"PEP layout fails relaxed check: {converted.Count(r => !r.ValidationTimedOut && !r.RelaxedValid)}; " - + $"fails strict benchmark check: {converted.Count(r => !r.ValidationTimedOut && !r.BaselineValid)}; " - + $"validation timed out: {converted.Count(r => r.ValidationTimedOut)}), " - + $"skipped {ordered.Count(r => r.Result.StartsWith("skipped"))}, " - + $"errors {ordered.Count(r => r.Result.StartsWith("error"))}." -); -if (converted.Count > 0) -{ - var sheet = converted.Sum(r => r.SheetArea); - var part = converted.Sum(r => r.PartArea); - Console.WriteLine( - $"PEP across converted nests: {converted.Sum(r => r.Sheets)} sheets, {sheet:F0} sq in of sheet, " - + $"{part:F0} sq in of parts, {100 * part / sheet:F1}% utilization." - ); -} -Console.WriteLine($"Summary: {csvPath}"); -return 0; - -static class PepConverter -{ - public static void Convert( - NestSummary summary, - PepModels.Nest pep, - QuantityMode quantityMode, - ReportRow row, - string nestPath - ) - { - var required = pep - .Drawings.GroupBy(d => d.Name, StringComparer.OrdinalIgnoreCase) - .ToDictionary(g => g.Key, g => g.Sum(d => d.QtyRequired), StringComparer.OrdinalIgnoreCase); - - var pepPlates = pep - .Plates.OrderBy(p => p.Name, StringComparer.OrdinalIgnoreCase) - .Select(p => (Plate: p, Parts: p.Parts.Where(IsRealPart).ToList())) - .Where(p => p.Parts.Count > 0) - .ToList(); - - if (pepPlates.Count == 0) - { - row.Result = "skipped (no nested parts)"; - return; - } - - var first = pepPlates[0].Plate; - var nest = new Nest(summary.Name) - { - Units = Units.Inches, - Customer = summary.Customer, - Thickness = first.Thickness, - Material = new Material(summary.MaterialNumber.ToString(), summary.MaterialGrade), - DateCreated = summary.DateCreated, - DateLastModified = DateTime.Now, - }; - - var drawings = new Dictionary(StringComparer.OrdinalIgnoreCase); - var drawingBoxes = new Dictionary(StringComparer.OrdinalIgnoreCase); - var loopShapes = new Dictionary(); - var warnings = new List(); - - foreach (var (pepPlate, pepParts) in pepPlates) - { - // PEP "PLATE SCALING = 60X120" is Y x X; OpenNest Size is (Width = Y, Length = X). - var plate = new Plate(pepPlate.Size.Height, pepPlate.Size.Width) - { - Quantity = System.Math.Max(1, pepPlate.Duplicates), - Quadrant = pepPlate.Quadrant is >= 1 and <= 4 ? pepPlate.Quadrant : 1, - PartSpacing = pepPlate.PartSpacing, - EdgeSpacing = new Spacing( - pepPlate.EdgeSpacing.Left, - pepPlate.EdgeSpacing.Bottom, - pepPlate.EdgeSpacing.Right, - pepPlate.EdgeSpacing.Top - ), - }; - - foreach (var pepPart in pepParts) - { - if (!loopShapes.TryGetValue(pepPart.Name, out var shape)) - { - var loop = - pep.Loops.FirstOrDefault(l => l.Name == pepPart.Name) - ?? throw new InvalidDataException($"Loop {pepPart.Name} not found"); - var program = ToProgram(loop); - shape = (program, CutBox(program)); - loopShapes.Add(pepPart.Name, shape); - } - - if (!drawings.TryGetValue(pepPart.DrawingName, out var drawing)) - { - drawing = new Drawing(pepPart.DrawingName, shape.Program) - { - Customer = summary.Customer, - Material = nest.Material, - Color = Drawing.GetNextColor(), - }; - drawings.Add(pepPart.DrawingName, drawing); - drawingBoxes.Add(pepPart.DrawingName, shape.CutBox); - nest.Drawings.Add(drawing); - if (!HasClosedPerimeter(shape.Program)) - warnings.Add($"{pepPart.DrawingName}: no closed outer contour; engines cannot place it"); - } - - // PEP can place one drawing through several loops, each starting at its own - // pierce point, so each loop's frame is a translation of the drawing's. - var drawingBox = drawingBoxes[pepPart.DrawingName]; - var delta = new Vector( - shape.CutBox.Left - drawingBox.Left, - shape.CutBox.Bottom - drawingBox.Bottom - ); - if ( - System.Math.Abs(shape.CutBox.Width - drawingBox.Width) > 0.01 - || System.Math.Abs(shape.CutBox.Length - drawingBox.Length) > 0.01 - ) - { - warnings.Add($"{pepPart.DrawingName}: loop {pepPart.Name} differs in size from the drawing's first loop"); - } - - // Same convention in both systems: rotate the program about its origin, then - // place that origin at the part location. - var part = new Part(drawing); - if (!OpenNest.Math.Tolerance.IsEqualTo(pepPart.Rotation, 0)) - part.Rotate(pepPart.Rotation); - part.Location = new Vector(pepPart.Location.X, pepPart.Location.Y) + delta.Rotate(pepPart.Rotation); - plate.Parts.Add(part); - } - - nest.Plates.Add(plate); - } - - nest.PlateDefaults.SetFromExisting(nest.Plates[0]); - nest.UpdateDrawingQuantities(); - - foreach (var drawing in nest.Drawings) - { - var pepRequired = required.GetValueOrDefault(drawing.Name); - var nested = drawing.Quantity.Nested; - drawing.Quantity.Required = - quantityMode == QuantityMode.Required && pepRequired > 0 ? pepRequired : nested; - if (pepRequired != nested) - row.QuantityMismatches.Add($"{drawing.Name} req {pepRequired} nested {nested}"); - } - - var unnested = required - .Where(r => r.Value > 0 && !drawings.ContainsKey(r.Key) && !IsSkeleton(r.Key)) - .Select(r => r.Key) - .ToList(); - foreach (var name in unnested) - row.QuantityMismatches.Add($"{name} req {required[name]} nested 0 (no geometry; not exported)"); - - nest.Notes = - $"Converted from PEP {summary.Name} ({summary.Status}; {summary.Comments}). " - + $"Plates are PEP's own layout. Quantities = PEP {quantityMode.ToString().ToLowerInvariant()} counts."; - - new NestWriter(nest).Write(nestPath); - // Report on the saved file (the writer rounds coordinates), which is what the benchmark reads. - FillReport(new NestReader(nestPath).Read(), row, warnings); - - var violationsPath = Path.ChangeExtension(nestPath, ".violations.txt"); - if (row.Violations.Count > 0) - File.WriteAllLines(violationsPath, row.Violations); - else - File.Delete(violationsPath); - row.Result = "ok"; - } - - private static void FillReport(Nest nest, ReportRow row, List warnings) - { - var plateRuns = new List<(Plate Plate, List Parts)>(); - foreach (var plate in nest.Plates) - for (var copy = 0; copy < plate.Quantity; copy++) - plateRuns.Add((plate, plate.Parts.ToList())); - - var requirements = nest.Drawings.ToDictionary( - d => d, - d => (d.Name, d.Quantity.Required) - ); - // PEP stores placements to ~4 decimals and spaces parts at exactly the nominal gap, which - // the validator's circumscribed arc polygons read as slightly short. A relaxed pass - // separates that from real conversion problems (overlaps, parts off the sheet). - var relaxedRuns = plateRuns - .Select(run => - { - var edge = run.Plate.EdgeSpacing; - var relaxed = new Plate(run.Plate.Size) - { - Quadrant = run.Plate.Quadrant, - PartSpacing = System.Math.Max(0, run.Plate.PartSpacing - RelaxedSpacingTolerance), - EdgeSpacing = new Spacing( - System.Math.Max(0, edge.Left - RelaxedEdgeTolerance), - System.Math.Max(0, edge.Bottom - RelaxedEdgeTolerance), - System.Math.Max(0, edge.Right - RelaxedEdgeTolerance), - System.Math.Max(0, edge.Top - RelaxedEdgeTolerance) - ), - }; - return (relaxed, run.Parts); - }) - .ToList(); - - row.Material = $"{nest.Material.Name} {nest.Material.Grade} {nest.Thickness:0.###}"; - row.Drawings = nest.Drawings.Count; - row.PartsRequested = nest.Drawings.Sum(d => d.Quantity.Required); - row.PartsNested = nest.Drawings.Sum(d => d.Quantity.Nested); - row.Sheets = nest.Plates.Sum(p => p.Quantity); - row.SheetSizes = string.Join( - " ", - nest.Plates.GroupBy(p => (p.Size.Width, p.Size.Length)) - .Select(g => $"{g.Key.Width:0.###}x{g.Key.Length:0.###}*{g.Sum(p => p.Quantity)}") - ); - row.PartSpacing = string.Join(" ", nest.Plates.Select(p => p.PartSpacing.ToString("0.###")).Distinct()); - row.EdgeSpacing = string.Join( - " ", - nest.Plates.Select(p => - $"{p.EdgeSpacing.Left:0.###}/{p.EdgeSpacing.Bottom:0.###}/{p.EdgeSpacing.Right:0.###}/{p.EdgeSpacing.Top:0.###}" - ) - .Distinct() - ); - row.SheetArea = nest.Plates.Sum(p => p.Size.Width * p.Size.Length * p.Quantity); - row.PartArea = nest.Drawings.Sum(d => d.Area * d.Quantity.Nested); - - // NestValidator can take minutes on parts with hundreds of outline segments and many - // holes; don't let one nest stall the batch. An abandoned check keeps running on a - // pool thread until the process exits. - var check = Task.Run(() => - ( - Strict: NestValidator.Validate(plateRuns, requirements), - Relaxed: NestValidator.Validate(relaxedRuns, requirements) - ) - ); - row.GeometryWarnings = warnings; - if (!check.Wait(ValidationTimeout)) - { - row.ValidationTimedOut = true; - row.Violations = warnings - .Prepend($"validation timed out after {ValidationTimeout.TotalSeconds:0}s (layout not checked)") - .ToList(); - return; - } - - var (validation, relaxedValidation) = check.Result; - row.BaselineValid = validation.Valid; - row.StrictViolations = validation.Violations.Count; - row.RelaxedValid = relaxedValidation.Valid; - row.Violations = relaxedValidation - .Violations.Select(v => "relaxed: " + v) - .Concat(warnings) - .Concat(validation.Violations.Select(v => "strict: " + v)) - .ToList(); - } - - private const double RelaxedSpacingTolerance = 0.025; - private const double RelaxedEdgeTolerance = 0.001; - private static readonly TimeSpan ValidationTimeout = TimeSpan.FromSeconds(60); - - private static Box CutBox(OpenNest.CNC.Program program) => - OpenNest.Converters.ConvertProgram.ToGeometry(program) - .Where(e => e.Layer != SpecialLayers.Rapid) - .Cast() - .GetBoundingBox(); - - private static bool HasClosedPerimeter(OpenNest.CNC.Program program) - { - var entities = OpenNest.Converters.ConvertProgram.ToGeometry(program) - .Where(e => e.Layer != SpecialLayers.Rapid) - .ToList(); - return entities.Count > 0 && new ShapeProfile(entities).Perimeter?.Area() > 1e-9; - } - - private static bool IsRealPart(PepModels.Part part) => - !part.IsDisplayOnly && !string.IsNullOrWhiteSpace(part.DrawingName) && !IsSkeleton(part.DrawingName); - - private static bool IsSkeleton(string name) => - name.StartsWith("Skeleton", StringComparison.OrdinalIgnoreCase); - - /// - /// Flattens a PEP loop (incremental, with sub-loop calls for holes) into an absolute OpenNest - /// program in the loop's own frame. Only contour cuts are kept as cut motion; display, scribe, - /// lead-in/out and destruct (slug-chopping) moves become rapids so they never shape the part - /// for nesting. Contours PEP leaves open by a micro-joint are closed. - /// - private static OpenNest.CNC.Program ToProgram(PepModels.Loop loop) - { - var codes = new List(); - Emit(loop, new PepVector(0, 0), codes); - var program = new OpenNest.CNC.Program(Mode.Absolute); - program.Codes.AddRange(CollapseRapids(CloseMicroJoints(codes))); - // Built absolute, stored incremental like CAD-imported drawings: the desktop renderer - // only applies a part's location to incremental programs. - program.Mode = Mode.Incremental; - return program; - } - - /// - /// Replaces each run of non-cut moves with one rapid onto the next contour's start and drops - /// trailing ones. Lead-in/out endpoints lie outside the part and a program's bounding box - /// counts rapid endpoints, so leaving them in would inflate the part for edge checks. - /// - private static IEnumerable CollapseRapids(IEnumerable codes) - { - var pos = new Vector(0, 0); - var pendingRapid = false; - foreach (var code in codes) - { - if (code is LinearMove or ArcMove) - { - if (pendingRapid) - yield return new RapidMove(pos); - pendingRapid = false; - yield return code; - } - else if (code is Motion) - { - pendingRapid = true; - } - - if (code is Motion motion) - pos = motion.EndPoint; - } - } - - /// Largest uncut tab (micro-joint) gap that is bridged to close a contour. - private const double MaxMicroJointGap = 0.25; - - /// - /// PEP leaves tabs uncut to hold parts and cutouts in place: the cut stops, jumps the tab - /// with a rapid, and carries on (e.g. a cutout cut as two halves 0.02 apart, or an outer - /// contour stopping 0.03 short of its start). The part still occupies that material, so - /// open cut runs are chained end to start across gaps up to - /// and each chain that closes into a loop gets a cut line across every tab. Links are - /// matched shortest gap first, and only closed loops are kept, so separate contours that - /// happen to lie close together (closed ones never take part) are not merged. - /// - private static IEnumerable CloseMicroJoints(List codes) - { - var runs = SplitCutRuns(codes); - var open = Enumerable - .Range(0, runs.Count) - .Where(i => runs[i].Start.DistanceTo(runs[i].End) > OpenNest.Math.Tolerance.Epsilon) - .ToList(); - - var next = new Dictionary(); - var previous = new Dictionary(); - var links = - from i in open - from j in open - let gap = runs[i].End.DistanceTo(runs[j].Start) - where gap <= MaxMicroJointGap - orderby gap - select (From: i, To: j); - - foreach (var (from, to) in links) - { - if (next.ContainsKey(from) || previous.ContainsKey(to)) - continue; - next[from] = to; - previous[to] = from; - } - - // Keep only links that close a loop; a chain that dead-ends is left as it was. - var cycleOf = new Dictionary>(); - foreach (var first in open.Where(next.ContainsKey)) - { - if (cycleOf.ContainsKey(first)) - continue; - - var cycle = new List { first }; - var current = next[first]; - while (current != first && next.TryGetValue(current, out var following) && !cycle.Contains(current)) - { - cycle.Add(current); - current = following; - } - - if (current != first) - continue; - - foreach (var index in cycle) - cycleOf[index] = cycle; - } - - var emitted = new HashSet(); - for (var i = 0; i < runs.Count; i++) - { - if (emitted.Contains(i)) - continue; - - if (!cycleOf.TryGetValue(i, out var cycle)) - { - emitted.Add(i); - yield return new RapidMove(runs[i].Start); - foreach (var code in runs[i].Codes) - yield return code; - continue; - } - - // Start the loop at the run that comes first in the program. - var offset = cycle.IndexOf(i); - yield return new RapidMove(runs[i].Start); - - for (var k = 0; k < cycle.Count; k++) - { - var run = runs[cycle[(offset + k) % cycle.Count]]; - var following = runs[cycle[(offset + k + 1) % cycle.Count]]; - emitted.Add(cycle[(offset + k) % cycle.Count]); - - foreach (var code in run.Codes) - yield return code; - - if (run.End.DistanceTo(following.Start) > OpenNest.Math.Tolerance.Epsilon) - yield return new LinearMove(following.Start) { Layer = LayerType.Cut }; - } - } - } - - private sealed record CutRun(Vector Start, Vector End, List Codes); - - /// - /// Splits an absolute program into runs of consecutive cut moves. Everything else only - /// positions the head, and rebuilds it afterwards. - /// - private static List SplitCutRuns(List codes) - { - var runs = new List(); - var pos = new Vector(0, 0); - List current = null; - var start = pos; - - foreach (var code in codes) - { - if (code is LinearMove or ArcMove) - { - if (current == null) - { - current = new List(); - start = pos; - } - current.Add(code); - } - else if (current != null) - { - runs.Add(new CutRun(start, pos, current)); - current = null; - } - - if (code is Motion motion) - pos = motion.EndPoint; - } - - if (current != null) - runs.Add(new CutRun(start, pos, current)); - - return runs; - } - - private static PepVector Emit(PepModels.Program source, PepVector start, List codes) - { - var pos = start; - var inDestructCut = false; - foreach (var code in source) - { - switch (code) - { - case PepCodes.Comment comment: - if (comment.Value.StartsWith("DESTRUCT CUT START", StringComparison.OrdinalIgnoreCase)) - inDestructCut = true; - else if (comment.Value.StartsWith("DESTRUCT CUT END", StringComparison.OrdinalIgnoreCase)) - inDestructCut = false; - break; - - case PepCodes.RapidMove rapid: - pos = Advance(pos, rapid.EndPoint, source.Mode); - codes.Add(new RapidMove(ToVector(pos))); - break; - - case PepCodes.LinearMove line: - pos = Advance(pos, line.EndPoint, source.Mode); - codes.Add( - line.Type == PepCodes.EntityType.Cut && !inDestructCut - ? new LinearMove(ToVector(pos)) { Layer = LayerType.Cut } - : new RapidMove(ToVector(pos)) - ); - break; - - case PepCodes.CircularMove arc: - var arcStart = pos; - pos = Advance(pos, arc.EndPoint, source.Mode); - var center = EquidistantCenter(arcStart, pos, Advance(arcStart, arc.CenterPoint, source.Mode)); - codes.Add( - arc.Type == PepCodes.EntityType.Cut && !inDestructCut - ? new ArcMove( - ToVector(pos), - ToVector(center), - arc.Rotation == PepLib.Enums.RotationType.CW - ? RotationType.CW - : RotationType.CCW - ) - { - Layer = LayerType.Cut, - } - : new RapidMove(ToVector(pos)) - ); - break; - - case PepCodes.SubProgramCall call when call.Loop != null: - // Incremental position carries through the sub-loop: the caller resumes - // where the sub-loop ended (e.g. identical holes called 13.8125 apart after - // a 0.1875 lead-in sit on a 14.000 pitch). - pos = Emit(call.Loop, pos, codes); - break; - } - } - return pos; - } - - /// - /// PEP stores some small arcs with a center that is not equidistant from both ends (e.g. a - /// 0.06 notch with radii 0.0300 and 0.0298). OpenNest rebuilds the arc from its end point, so - /// its start would miss the previous move and break the contour. Projecting the center onto - /// the chord's perpendicular bisector keeps both endpoints exact. Full circles are unchanged. - /// - private static PepVector EquidistantCenter(PepVector start, PepVector end, PepVector center) - { - var chordX = end.X - start.X; - var chordY = end.Y - start.Y; - var chord = System.Math.Sqrt(chordX * chordX + chordY * chordY); - if (chord < 1e-9) - return center; - - var midX = (start.X + end.X) / 2; - var midY = (start.Y + end.Y) / 2; - var normalX = -chordY / chord; - var normalY = chordX / chord; - var along = (center.X - midX) * normalX + (center.Y - midY) * normalY; - return new PepVector(midX + along * normalX, midY + along * normalY); - } - - private static PepVector Advance(PepVector current, PepVector offset, PepLib.Enums.ProgrammingMode mode) => - mode == PepLib.Enums.ProgrammingMode.Incremental ? current + offset : offset; - - private static Vector ToVector(PepVector v) => new(v.X, v.Y); -} - -enum QuantityMode -{ - Nested, - Required, -} - -sealed record NestSummary( - string Name, - DateTime DateCreated, - string Status, - string Comments, - string Customer, - int MaterialNumber, - string MaterialGrade, - string Application -); - -sealed class Options -{ - public string OutputDirectory; - public int Year = 2026; - public string ApiBaseUrl = "http://10.10.100.134:8085"; - public HashSet Nests = new(StringComparer.OrdinalIgnoreCase); - public HashSet Statuses = new(StringComparer.OrdinalIgnoreCase); - public QuantityMode Quantity = QuantityMode.Nested; - public int Parallel = 4; - public bool Force; - - public static Options Parse(string[] args) - { - var o = new Options(); - for (var i = 0; i < args.Length; i++) - { - switch (args[i]) - { - case "--year" when i + 1 < args.Length: - o.Year = int.Parse(args[++i], CultureInfo.InvariantCulture); - break; - case "--api" when i + 1 < args.Length: - o.ApiBaseUrl = args[++i]; - break; - case "--nests" when i + 1 < args.Length: - o.Nests.UnionWith(SplitList(args[++i])); - break; - case "--status" when i + 1 < args.Length: - o.Statuses.UnionWith(SplitList(args[++i])); - break; - case "--quantity" when i + 1 < args.Length: - if (!Enum.TryParse(args[++i], ignoreCase: true, out o.Quantity)) - return null; - break; - case "--parallel" when i + 1 < args.Length: - o.Parallel = System.Math.Max(1, int.Parse(args[++i], CultureInfo.InvariantCulture)); - break; - case "--force": - o.Force = true; - break; - default: - if (args[i].StartsWith("--") || o.OutputDirectory != null) - return null; - o.OutputDirectory = Path.GetFullPath(args[i]); - break; - } - } - return o.OutputDirectory == null ? null : o; - } - - private static IEnumerable SplitList(string value) => - value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); -} - -sealed class ReportRow -{ - public string Nest; - public string PepStatus; - public string Result = ""; - public string Material = ""; - public int Drawings; - public int PartsRequested; - public int PartsNested; - public int Sheets; - public string SheetSizes = ""; - public string PartSpacing = ""; - public string EdgeSpacing = ""; - public double SheetArea; - public double PartArea; - public bool BaselineValid; - public int StrictViolations; - public bool RelaxedValid; - public bool ValidationTimedOut; - public List Violations = new(); - public List GeometryWarnings = new(); - public List QuantityMismatches = new(); - - public static void WriteCsv(string path, IEnumerable rows) - { - var sb = new StringBuilder(); - sb.AppendLine( - "Nest,PepStatus,Result,Material,Drawings,PartsRequested,PartsNested,Sheets,SheetSizes," - + "PartSpacing,EdgeSpacing(L/B/R/T),SheetArea,PartArea,Utilization%,RelaxedValid," - + "StrictValid,StrictViolations,GeometryWarnings,Violations,QuantityMismatches" - ); - foreach (var r in rows) - { - var utilization = r.SheetArea > 0 ? 100 * r.PartArea / r.SheetArea : 0; - sb.AppendLine( - string.Join( - ",", - Csv(r.Nest), - Csv(r.PepStatus), - Csv(r.Result), - Csv(r.Material), - r.Drawings, - r.PartsRequested, - r.PartsNested, - r.Sheets, - Csv(r.SheetSizes), - Csv(r.PartSpacing), - Csv(r.EdgeSpacing), - r.SheetArea.ToString("F2"), - r.PartArea.ToString("F2"), - utilization.ToString("F2"), - r.Result != "ok" ? "" : r.ValidationTimedOut ? "timeout" : r.RelaxedValid.ToString(), - r.Result != "ok" ? "" : r.ValidationTimedOut ? "timeout" : r.BaselineValid.ToString(), - r.Result != "ok" || r.ValidationTimedOut ? "" : r.StrictViolations.ToString(), - Csv(string.Join(" | ", r.GeometryWarnings)), - Csv(string.Join(" | ", r.Violations.Take(5)) + (r.Violations.Count > 5 ? $" | +{r.Violations.Count - 5} more" : "")), - Csv(string.Join(" | ", r.QuantityMismatches)) - ) - ); - } - File.WriteAllText(path, sb.ToString()); - } - - private static string Csv(string value) => - value.IndexOfAny(new[] { ',', '"', '\n', '\r' }) >= 0 - ? "\"" + value.Replace("\"", "\"\"") + "\"" - : value; -} diff --git a/tools/StreamGravographJob/Program.cs b/tools/StreamGravographJob/Program.cs deleted file mode 100644 index 34c9a6b..0000000 --- a/tools/StreamGravographJob/Program.cs +++ /dev/null @@ -1,343 +0,0 @@ -using System; -using System.ComponentModel; -using System.Diagnostics; -using System.IO; -using System.IO.Ports; -using System.Runtime.InteropServices; -using System.Threading; -using Microsoft.Win32.SafeHandles; - -if (args.Length < 2) -{ - Console.Error.WriteLine("Usage:"); - Console.Error.WriteLine( - " StreamGravographJob [chunk=256] [flow=rtscts|xonxoff|none]" - ); - Console.Error.WriteLine( - " StreamGravographJob --gen # name: testA | testB | miniB | miniSquare" - ); - Console.Error.WriteLine(" StreamGravographJob --inspect-nest "); - Console.Error.WriteLine(" StreamGravographJob --from-nest "); - return 2; -} - -// Inspect a .nest file: extract polylines via the post-processor pipeline and -// report dimensions / bounding box / pen-up travel — no bytes written. -if (args[0] == "--inspect-nest") -{ - if (args.Length < 2) - { - Console.Error.WriteLine("--inspect-nest requires "); - return 2; - } - var nestPath = args[1]; - if (!File.Exists(nestPath)) - { - Console.Error.WriteLine($"Not found: {nestPath}"); - return 3; - } - - using var fs = new FileStream(nestPath, FileMode.Open, FileAccess.Read); - var reader = new OpenNest.IO.NestReader(fs); - var nest = reader.Read(); - - Console.WriteLine($"Nest: {nest.Name}"); - Console.WriteLine($"Units: {nest.Units}"); - Console.WriteLine($"Plates: {nest.Plates.Count}"); - var plateIdx = 0; - foreach (var plate in nest.Plates) - { - plateIdx++; - Console.WriteLine( - $" Plate {plateIdx}: size={plate.Size.Length} x {plate.Size.Width}, quadrant={plate.Quadrant}, parts={plate.Parts.Count}" - ); - } - - var polylines = new OpenNest.Posts.GravographIS.NestPolylineExtractor().Extract(nest); - if (polylines.Count == 0) - { - Console.WriteLine("No polylines extracted."); - return 0; - } - - double minX = double.PositiveInfinity, - minY = double.PositiveInfinity; - double maxX = double.NegativeInfinity, - maxY = double.NegativeInfinity; - int totalPts = 0; - foreach (var p in polylines) - { - foreach (var v in p) - { - if (v.X < minX) - minX = v.X; - if (v.X > maxX) - maxX = v.X; - if (v.Y < minY) - minY = v.Y; - if (v.Y > maxY) - maxY = v.Y; - } - totalPts += p.Count; - } - Console.WriteLine($"Polylines: {polylines.Count}, total points: {totalPts}"); - Console.WriteLine( - $"Bounding box (inches): X ∈ [{minX:F3}, {maxX:F3}] Y ∈ [{minY:F3}, {maxY:F3}]" - ); - Console.WriteLine($"Extents: {maxX - minX:F3}\" × {maxY - minY:F3}\""); - - // After running the pre-pass (stitch + reorder from origin) — what the writer will actually consume. - var prepared = OpenNest.Posts.GravographIS.PolylinePrePass.Prepare(polylines); - Console.WriteLine($"After stitch+reorder: {prepared.Count} polylines"); - - Console.WriteLine(); - Console.WriteLine("--- Vertex dump (prepared, upper-left origin, with segment deltas) ---"); - var pi = 0; - foreach (var poly in prepared) - { - pi++; - Console.WriteLine($"Polyline {pi}: {poly.Count} points"); - var cumX = 0.0; - var cumY = 0.0; - for (var i = 0; i < poly.Count; i++) - { - var v = poly[i]; - if (i == 0) - { - Console.WriteLine( - $" [{i}] ({v.X, 7:F3}, {v.Y, 7:F3}) first DR travel from upper-left origin=({v.X, +7:F3}, {v.Y, +7:F3})" - ); - } - else - { - var dx = v.X - poly[i - 1].X; - var dy = v.Y - poly[i - 1].Y; - cumX += dx; - cumY += dy; - Console.WriteLine( - $" [{i}] ({v.X, 7:F3}, {v.Y, 7:F3}) Δ=({dx, +7:F3}, {dy, +7:F3}) cum from origin=({cumX, +7:F3}, {cumY, +7:F3})" - ); - } - } - } - return 0; -} - -// Convert a .nest file to a .prn job via the full post-processor pipeline. -if (args[0] == "--from-nest") -{ - if (args.Length < 3) - { - Console.Error.WriteLine("--from-nest requires "); - return 2; - } - var nestPath = args[1]; - var outFile = args[2]; - if (!File.Exists(nestPath)) - { - Console.Error.WriteLine($"Not found: {nestPath}"); - return 3; - } - - using var fs = new FileStream(nestPath, FileMode.Open, FileAccess.Read); - var nest = new OpenNest.IO.NestReader(fs).Read(); - var post = new OpenNest.Posts.GravographIS.GravographISPostProcessor(); - post.Post(nest, outFile); - - var size = new FileInfo(outFile).Length; - Console.WriteLine($"Wrote {size} bytes → {outFile}"); - return 0; -} - -// Generator mode: run the live writer to produce a captured-test file on disk. -if (args[0] == "--gen") -{ - if (args.Length < 3) - { - Console.Error.WriteLine("--gen requires "); - return 2; - } - var preset = args[1]; - var outFile = args[2]; - var polylines = preset.ToLowerInvariant() switch - { - "testa" => - new System.Collections.Generic.List> - { - new[] { new OpenNest.Geometry.Vector(1, 1), new OpenNest.Geometry.Vector(1, 3) }, - }, - "testb" => - new System.Collections.Generic.List> - { - new[] { new OpenNest.Geometry.Vector(1, 1), new OpenNest.Geometry.Vector(1, 3) }, - new[] { new OpenNest.Geometry.Vector(4, 1), new OpenNest.Geometry.Vector(4, 3) }, - new[] { new OpenNest.Geometry.Vector(4, 5), new OpenNest.Geometry.Vector(4, 7) }, - new[] { new OpenNest.Geometry.Vector(1, 5), new OpenNest.Geometry.Vector(1, 7) }, - }, - // Same 4-polyline topology as testB (vertical lines + diagonal PU travels between them), - // shrunk to a 0.5" × 1.5" footprint so it stays right near the operator-set work origin. - "minib" => - new System.Collections.Generic.List> - { - new[] { new OpenNest.Geometry.Vector(0, 0), new OpenNest.Geometry.Vector(0, 0.5) }, - new[] - { - new OpenNest.Geometry.Vector(0.5, 0), - new OpenNest.Geometry.Vector(0.5, 0.5), - }, - new[] - { - new OpenNest.Geometry.Vector(0.5, 1), - new OpenNest.Geometry.Vector(0.5, 1.5), - }, - new[] { new OpenNest.Geometry.Vector(0, 1), new OpenNest.Geometry.Vector(0, 1.5) }, - }, - // Closed 0.5" square as a SINGLE polyline of 5 points → 4-segment PD packet. - // Exercises multi-segment PD (one FF FD 50 44 00 00 followed by 4 records, - // no intermediate lifts) and bi-directional motion (X+, Y+, X−, Y−). - // Returns the head to its starting point so no manual jog needed after. - "minisquare" => - new System.Collections.Generic.List> - { - new[] - { - new OpenNest.Geometry.Vector(0, 0), - new OpenNest.Geometry.Vector(0.5, 0), - new OpenNest.Geometry.Vector(0.5, 0.5), - new OpenNest.Geometry.Vector(0, 0.5), - new OpenNest.Geometry.Vector(0, 0), - }, - }, - _ => throw new ArgumentException( - $"Unknown preset '{preset}' (try testA, testB, miniB, or miniSquare)." - ), - }; - - using var outFs = new FileStream(outFile, FileMode.Create, FileAccess.Write); - new OpenNest.Posts.GravographIS.GravographISWriter().Write(polylines, outFs); - Console.WriteLine($"Wrote {new FileInfo(outFile).Length} bytes via live writer → {outFile}"); - return 0; -} - -var file = args[0]; -var portName = args[1]; -var chunk = args.Length > 2 ? int.Parse(args[2]) : 256; -var flowArg = args.Length > 3 ? args[3] : "rtscts"; - -var handshake = flowArg.ToLowerInvariant() switch -{ - "rtscts" or "rts" or "cts" => Handshake.RequestToSend, - "xonxoff" or "xon" or "xoff" => Handshake.XOnXOff, - "none" => Handshake.None, - _ => throw new ArgumentException($"Unknown flow control '{flowArg}'."), -}; - -if (!File.Exists(file)) -{ - Console.Error.WriteLine($"File not found: {file}"); - return 3; -} - -var bytes = File.ReadAllBytes(file); -Console.WriteLine($"File: {file}"); -Console.WriteLine($"Size: {bytes.Length} bytes"); -Console.WriteLine( - $"Header: {BitConverter.ToString(bytes, 0, Math.Min(7, bytes.Length)).Replace('-', ' ')}" -); - -var ports = SerialPort.GetPortNames(); -Array.Sort(ports); -Console.WriteLine($"Available COM ports: {string.Join(", ", ports)}"); -if (Array.IndexOf(ports, portName) < 0) -{ - Console.Error.WriteLine($"{portName} not in available ports."); - return 4; -} - -using var port = new SerialPort(portName, 9600, Parity.None, 8, StopBits.One) -{ - Handshake = handshake, - WriteTimeout = 30000, - ReadTimeout = 30000, - WriteBufferSize = 4096, - DtrEnable = true, -}; - -// Probe with the same CreateFile flags SerialStream uses, in this same process, -// so we can tell SerialStream-specific failures apart from process-level access denials. -{ - const uint GENERIC_RW = 0x80000000u | 0x40000000u; - const uint OPEN_EXISTING = 3; - const uint FILE_FLAG_OVERLAPPED = 0x40000000u; - var devName = @"\\.\" + portName; - var handle = NativeMethods.CreateFileW( - devName, - GENERIC_RW, - 0, - IntPtr.Zero, - OPEN_EXISTING, - FILE_FLAG_OVERLAPPED, - IntPtr.Zero - ); - var err = Marshal.GetLastWin32Error(); - if (handle.IsInvalid) - { - Console.WriteLine( - $"CreateFile(\"{devName}\", overlapped, exclusive) FAILED: win32={err} ({new Win32Exception(err).Message})" - ); - } - else - { - Console.WriteLine($"CreateFile(\"{devName}\", overlapped, exclusive) OK — closing."); - handle.Close(); - } -} - -Console.WriteLine($"Opening {portName} 9600 8N1 handshake={handshake}..."); -port.Open(); -Console.WriteLine("Opened."); - -var sw = Stopwatch.StartNew(); -try -{ - for (var i = 0; i < bytes.Length; i += chunk) - { - var n = Math.Min(chunk, bytes.Length - i); - port.Write(bytes, i, n); - } - try - { - port.BaseStream.Flush(); - } - catch - { /* advisory */ - } - Thread.Sleep(500); -} -finally -{ - sw.Stop(); - port.Close(); -} - -Console.WriteLine($"Sent {bytes.Length} bytes in {sw.ElapsedMilliseconds} ms. Port closed."); -return 0; - -internal static class NativeMethods -{ - [DllImport( - "kernel32.dll", - SetLastError = true, - CharSet = CharSet.Unicode, - EntryPoint = "CreateFileW" - )] - internal static extern SafeFileHandle CreateFileW( - string lpFileName, - uint dwDesiredAccess, - uint dwShareMode, - IntPtr lpSecurityAttributes, - uint dwCreationDisposition, - uint dwFlagsAndAttributes, - IntPtr hTemplateFile - ); -} diff --git a/tools/StreamGravographJob/StreamGravographJob.csproj b/tools/StreamGravographJob/StreamGravographJob.csproj deleted file mode 100644 index eb28800..0000000 --- a/tools/StreamGravographJob/StreamGravographJob.csproj +++ /dev/null @@ -1,14 +0,0 @@ - - - Exe - net8.0-windows - enable - enable - OpenNest.Tools.StreamGravographJob - - - - - - -