feat: add CincinnatiPostProcessor implementing IPostProcessor

Orchestrates CincinnatiPreambleWriter and CincinnatiSheetWriter to produce
a complete Cincinnati CNC output file from a Nest; includes 4 integration tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-22 23:41:06 -04:00
parent 8c3659a439
commit ca8a0942ab
2 changed files with 199 additions and 0 deletions

View File

@@ -0,0 +1,77 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace OpenNest.Posts.Cincinnati
{
public sealed class CincinnatiPostProcessor : IPostProcessor
{
public string Name => "Cincinnati CL-707";
public string Author => "OpenNest";
public string Description => "Cincinnati CL-707/CL-800/CL-900/CL-940/CLX family";
public CincinnatiPostConfig Config { get; }
public CincinnatiPostProcessor(CincinnatiPostConfig config)
{
Config = config;
}
public void Post(Nest nest, Stream outputStream)
{
// 1. Create variable manager and register standard variables
var vars = CreateVariableManager();
// 2. Filter to non-empty plates
var plates = nest.Plates
.Where(p => p.Parts.Count > 0)
.ToList();
// 3. Create writers
var preamble = new CincinnatiPreambleWriter(Config);
var sheetWriter = new CincinnatiSheetWriter(Config, vars);
// 4. Build material description from first plate
var material = plates.FirstOrDefault()?.Material;
var materialDesc = material != null
? $"{material.Name}{(string.IsNullOrEmpty(material.Grade) ? "" : $", {material.Grade}")}"
: "";
// 5. Write to stream
using var writer = new StreamWriter(outputStream, Encoding.UTF8, 1024, leaveOpen: true);
// Main program
preamble.WriteMainProgram(writer, nest.Name ?? "NEST", materialDesc, plates.Count);
// Variable declaration subprogram
preamble.WriteVariableDeclaration(writer, vars);
// Sheet subprograms
for (var i = 0; i < plates.Count; i++)
{
var sheetIndex = i + 1;
var subNumber = Config.SheetSubprogramStart + i;
sheetWriter.Write(writer, plates[i], nest.Name ?? "NEST", sheetIndex, subNumber);
}
writer.Flush();
}
public void Post(Nest nest, string outputFile)
{
using var fs = new FileStream(outputFile, FileMode.Create, FileAccess.Write);
Post(nest, fs);
}
private ProgramVariableManager CreateVariableManager()
{
var vars = new ProgramVariableManager();
vars.GetOrCreate("ProcessFeedrate", 148); // Set by G89, no expression
vars.GetOrCreate("LeadInFeedrate", 126, $"[#148*{Config.LeadInFeedratePercent}]");
vars.GetOrCreate("LeadInArcLine2Feedrate", 127, $"[#148*{Config.LeadInArcLine2FeedratePercent}]");
vars.GetOrCreate("CircleFeedrate", 128, Config.CircleFeedrateMultiplier.ToString("0.#"));
return vars;
}
}
}

View File

@@ -0,0 +1,122 @@
using System.IO;
using System.Text;
using OpenNest.CNC;
using OpenNest.Geometry;
using OpenNest.Posts.Cincinnati;
namespace OpenNest.Tests.Cincinnati;
public class CincinnatiPostProcessorTests
{
[Fact]
public void Post_ProducesOutput_ForSinglePlateNest()
{
var nest = CreateTestNest();
var config = new CincinnatiPostConfig
{
ConfigurationName = "CL940",
DefaultLibraryFile = "MS135N2PANEL.lib",
PostedAccuracy = 4
};
var post = new CincinnatiPostProcessor(config);
using var ms = new MemoryStream();
post.Post(nest, ms);
var output = Encoding.UTF8.GetString(ms.ToArray());
// Main program elements
Assert.Contains("( NEST TestNest )", output);
Assert.Contains("( CONFIGURATION - CL940 )", output);
Assert.Contains("G20", output);
Assert.Contains("M30 (END OF MAIN)", output);
// Variable declaration
Assert.Contains(":100", output);
Assert.Contains("#126=", output);
// Sheet subprogram
Assert.Contains(":101", output);
Assert.Contains("( Sheet 1 )", output);
Assert.Contains("G84", output);
Assert.Contains("M99", output);
}
[Fact]
public void Post_ImplementsIPostProcessor()
{
var post = new CincinnatiPostProcessor(new CincinnatiPostConfig());
IPostProcessor pp = post;
Assert.Equal("Cincinnati CL-707", pp.Name);
Assert.Equal("OpenNest", pp.Author);
}
[Fact]
public void Post_SkipsEmptyPlates()
{
var nest = new Nest("TestNest");
nest.Plates.Add(new Plate(48, 96)); // empty plate
var plate2 = new Plate(48, 96);
plate2.Parts.Add(new Part(new Drawing("Part1", CreateSquareProgram())));
nest.Plates.Add(plate2);
var config = new CincinnatiPostConfig { PostedAccuracy = 4 };
var post = new CincinnatiPostProcessor(config);
using var ms = new MemoryStream();
post.Post(nest, ms);
var output = Encoding.UTF8.GetString(ms.ToArray());
// Should only have one sheet subprogram call in main
Assert.Contains("N1M98 P101 (SHEET 1)", output);
Assert.DoesNotContain("SHEET 2", output);
}
[Fact]
public void Post_ToFile_CreatesFile()
{
var nest = CreateTestNest();
var config = new CincinnatiPostConfig { PostedAccuracy = 4 };
var post = new CincinnatiPostProcessor(config);
var tempFile = Path.GetTempFileName() + ".CNC";
try
{
post.Post(nest, tempFile);
Assert.True(File.Exists(tempFile));
var content = File.ReadAllText(tempFile);
Assert.Contains("M30", content);
}
finally
{
if (File.Exists(tempFile))
File.Delete(tempFile);
}
}
private static Nest CreateTestNest()
{
var nest = new Nest("TestNest");
var drawing = new Drawing("Square", CreateSquareProgram());
nest.Drawings.Add(drawing);
var plate = new Plate(48.0, 96.0);
plate.Parts.Add(new Part(drawing, new Vector(10, 10)));
nest.Plates.Add(plate);
return nest;
}
private static Program CreateSquareProgram()
{
var pgm = new Program();
pgm.Codes.Add(new RapidMove(0, 0));
pgm.Codes.Add(new LinearMove(2, 0));
pgm.Codes.Add(new LinearMove(2, 2));
pgm.Codes.Add(new LinearMove(0, 2));
pgm.Codes.Add(new LinearMove(0, 0));
return pgm;
}
}