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).
This commit is contained in:
@@ -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=<path>` 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 `<nest>.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
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../OpenNest.Benchmark/OpenNest.Benchmark.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -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 <dxf-directory> <quantities.xlsx> <settings.nest> <new-output-directory> <engine|import-only>"
|
||||
);
|
||||
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<Plate> { 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<object>();
|
||||
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<string, string> 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<string> Validate(Nest nest, IReadOnlyDictionary<string, int> 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<NestJobProgress>
|
||||
{
|
||||
public void Report(NestJobProgress value)
|
||||
{
|
||||
if (value.Stage == NestJobStage.PlateCommitted)
|
||||
Console.WriteLine(
|
||||
$"Committed {value.CommittedPlates} sheets / {value.CommittedParts} parts"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<!-- PepLib.Core lives in the PepApi.Core repo; override with -p:PepLibProject=<path> if it is cloned elsewhere. -->
|
||||
<PepLibProject Condition="'$(PepLibProject)' == ''">$(MSBuildThisFileDirectory)..\..\..\PepApi.Core\PepLib.Core\PepLib.Core.csproj</PepLibProject>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../OpenNest.Benchmark/OpenNest.Benchmark.csproj" />
|
||||
<ProjectReference Include="$(PepLibProject)" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -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 <output-directory> [options]
|
||||
|
||||
--year <yyyy> PEP year to export (default 2026)
|
||||
--api <url> 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 <n> Nests converted at once (default 4)
|
||||
--force Re-download and re-convert nests that already exist
|
||||
|
||||
Writes <output>/<nest>.nest, caches the raw files in <output>/pep/, and writes a
|
||||
per-nest summary to <output>/pep-baseline.csv. Benchmark the output folder with:
|
||||
OpenNest.Benchmark <output-directory> --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<NestSummary> summaries;
|
||||
try
|
||||
{
|
||||
summaries =
|
||||
await http.GetFromJsonAsync<List<NestSummary>>($"nests/{options.Year}", json)
|
||||
?? new List<NestSummary>();
|
||||
}
|
||||
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<ReportRow>();
|
||||
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<string, Drawing>(StringComparer.OrdinalIgnoreCase);
|
||||
var drawingBoxes = new Dictionary<string, Box>(StringComparer.OrdinalIgnoreCase);
|
||||
var loopShapes = new Dictionary<string, (OpenNest.CNC.Program Program, Box CutBox)>();
|
||||
var warnings = new List<string>();
|
||||
|
||||
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<string> warnings)
|
||||
{
|
||||
var plateRuns = new List<(Plate Plate, List<Part> 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<IBoundable>()
|
||||
.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);
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
private static OpenNest.CNC.Program ToProgram(PepModels.Loop loop)
|
||||
{
|
||||
var codes = new List<ICode>();
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
private static IEnumerable<ICode> CollapseRapids(IEnumerable<ICode> 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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Largest uncut tab (micro-joint) gap that is bridged to close a contour.</summary>
|
||||
private const double MaxMicroJointGap = 0.25;
|
||||
|
||||
/// <summary>
|
||||
/// 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 <see cref="MaxMicroJointGap"/>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
private static IEnumerable<ICode> CloseMicroJoints(List<ICode> 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<int, int>();
|
||||
var previous = new Dictionary<int, int>();
|
||||
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<int, List<int>>();
|
||||
foreach (var first in open.Where(next.ContainsKey))
|
||||
{
|
||||
if (cycleOf.ContainsKey(first))
|
||||
continue;
|
||||
|
||||
var cycle = new List<int> { 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<int>();
|
||||
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<ICode> Codes);
|
||||
|
||||
/// <summary>
|
||||
/// Splits an absolute program into runs of consecutive cut moves. Everything else only
|
||||
/// positions the head, and <see cref="CollapseRapids"/> rebuilds it afterwards.
|
||||
/// </summary>
|
||||
private static List<CutRun> SplitCutRuns(List<ICode> codes)
|
||||
{
|
||||
var runs = new List<CutRun>();
|
||||
var pos = new Vector(0, 0);
|
||||
List<ICode> current = null;
|
||||
var start = pos;
|
||||
|
||||
foreach (var code in codes)
|
||||
{
|
||||
if (code is LinearMove or ArcMove)
|
||||
{
|
||||
if (current == null)
|
||||
{
|
||||
current = new List<ICode>();
|
||||
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<ICode> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<string> Nests = new(StringComparer.OrdinalIgnoreCase);
|
||||
public HashSet<string> 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<string> 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<string> Violations = new();
|
||||
public List<string> GeometryWarnings = new();
|
||||
public List<string> QuantityMismatches = new();
|
||||
|
||||
public static void WriteCsv(string path, IEnumerable<ReportRow> 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;
|
||||
}
|
||||
@@ -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 <file.prn> <COMx> [chunk=256] [flow=rtscts|xonxoff|none]"
|
||||
);
|
||||
Console.Error.WriteLine(
|
||||
" StreamGravographJob --gen <name> <outfile.prn> # name: testA | testB | miniB | miniSquare"
|
||||
);
|
||||
Console.Error.WriteLine(" StreamGravographJob --inspect-nest <file.nest>");
|
||||
Console.Error.WriteLine(" StreamGravographJob --from-nest <file.nest> <outfile.prn>");
|
||||
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 <file.nest>");
|
||||
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 <file.nest> <outfile.prn>");
|
||||
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 <name> <outfile>");
|
||||
return 2;
|
||||
}
|
||||
var preset = args[1];
|
||||
var outFile = args[2];
|
||||
var polylines = preset.ToLowerInvariant() switch
|
||||
{
|
||||
"testa" =>
|
||||
new System.Collections.Generic.List<System.Collections.Generic.IReadOnlyList<OpenNest.Geometry.Vector>>
|
||||
{
|
||||
new[] { new OpenNest.Geometry.Vector(1, 1), new OpenNest.Geometry.Vector(1, 3) },
|
||||
},
|
||||
"testb" =>
|
||||
new System.Collections.Generic.List<System.Collections.Generic.IReadOnlyList<OpenNest.Geometry.Vector>>
|
||||
{
|
||||
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<System.Collections.Generic.IReadOnlyList<OpenNest.Geometry.Vector>>
|
||||
{
|
||||
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<System.Collections.Generic.IReadOnlyList<OpenNest.Geometry.Vector>>
|
||||
{
|
||||
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
|
||||
);
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<RootNamespace>OpenNest.Tools.StreamGravographJob</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.IO.Ports" Version="8.0.0" />
|
||||
<ProjectReference Include="..\..\OpenNest.Posts.GravographIS\OpenNest.Posts.GravographIS.csproj" />
|
||||
<ProjectReference Include="..\..\OpenNest.IO\OpenNest.IO.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user