diff --git a/OpenNest.Posts.Cincinnati/CincinnatiSheetWriter.cs b/OpenNest.Posts.Cincinnati/CincinnatiSheetWriter.cs new file mode 100644 index 0000000..0f280b8 --- /dev/null +++ b/OpenNest.Posts.Cincinnati/CincinnatiSheetWriter.cs @@ -0,0 +1,173 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using OpenNest.CNC; +using OpenNest.Geometry; + +namespace OpenNest.Posts.Cincinnati; + +/// +/// Emits one Cincinnati-format sheet subprogram per plate. +/// Splits each part's codes at RapidMove boundaries to handle multi-contour parts. +/// +public sealed class CincinnatiSheetWriter +{ + private readonly CincinnatiPostConfig _config; + private readonly ProgramVariableManager _vars; + private readonly CoordinateFormatter _fmt; + private readonly CincinnatiFeatureWriter _featureWriter; + + public CincinnatiSheetWriter(CincinnatiPostConfig config, ProgramVariableManager vars) + { + _config = config; + _vars = vars; + _fmt = new CoordinateFormatter(config.PostedAccuracy); + _featureWriter = new CincinnatiFeatureWriter(config); + } + + /// + /// Writes a complete sheet subprogram for the given plate. + /// + public void Write(TextWriter w, Plate plate, string nestName, int sheetIndex, int subNumber) + { + if (plate.Parts.Count == 0) + return; + + var width = plate.Size.Width; + var length = plate.Size.Length; + var sheetDiagonal = System.Math.Sqrt(width * width + length * length); + var libraryFile = _config.DefaultLibraryFile ?? ""; + var varDeclSub = _config.VariableDeclarationSubprogram; + var partCount = plate.Parts.Count(p => !p.BaseDrawing.IsCutOff); + + // 1. Sheet header + w.WriteLine("(*****************************************************)"); + w.WriteLine($"( START OF {nestName}.{sheetIndex:D3} )"); + w.WriteLine($":{subNumber}"); + w.WriteLine($"( Sheet {sheetIndex} )"); + w.WriteLine($"( Layout {sheetIndex} )"); + w.WriteLine($"( SHEET NAME = {_fmt.FormatCoord(length)} X {_fmt.FormatCoord(width)} )"); + w.WriteLine($"( Total parts on sheet = {partCount} )"); + w.WriteLine($"#{_config.SheetWidthVariable}={_fmt.FormatCoord(width)}(SHEET WIDTH FOR CUTOFFS)"); + w.WriteLine($"#{_config.SheetLengthVariable}={_fmt.FormatCoord(length)}(SHEET LENGTH FOR CUTOFFS)"); + + // 2. Coordinate setup + w.WriteLine("M42"); + w.WriteLine("N10000"); + w.WriteLine("G92X#5021Y#5022"); + if (!string.IsNullOrEmpty(libraryFile)) + w.WriteLine($"G89 P {libraryFile}"); + w.WriteLine($"M98 P{varDeclSub} (Variable Declaration)"); + w.WriteLine("G90"); + w.WriteLine("M47(CPT)"); + if (!string.IsNullOrEmpty(libraryFile)) + w.WriteLine($"G89 P {libraryFile}"); + w.WriteLine("GOTO1( Goto Feature )"); + + // 3. Order parts: non-cutoff sorted by Bottom then Left, cutoffs last + var nonCutoffParts = plate.Parts + .Where(p => !p.BaseDrawing.IsCutOff) + .OrderBy(p => p.Bottom) + .ThenBy(p => p.Left) + .ToList(); + + var cutoffParts = plate.Parts + .Where(p => p.BaseDrawing.IsCutOff) + .ToList(); + + var allParts = nonCutoffParts.Concat(cutoffParts).ToList(); + + // 4. Multi-contour splitting + var features = new List<(Part part, List codes)>(); + foreach (var part in allParts) + { + List current = null; + foreach (var code in part.Program.Codes) + { + if (code is RapidMove) + { + if (current != null) + features.Add((part, current)); + current = new List { code }; + } + else + { + current ??= new List(); + current.Add(code); + } + } + if (current != null && current.Count > 0) + features.Add((part, current)); + } + + // 5. Emit features + var lastPartName = ""; + for (var i = 0; i < features.Count; i++) + { + var (part, codes) = features[i]; + var partName = part.BaseDrawing.Name; + var isFirstFeatureOfPart = partName != lastPartName; + var isSafetyHeadraise = partName != lastPartName && lastPartName != ""; + var isLastFeature = i == features.Count - 1; + + // Feature numbering: first = FeatureLineNumberStart, then 1002, 1003, etc. + var featureNumber = i == 0 + ? _config.FeatureLineNumberStart + : 1000 + i + 1; + + // Compute cut distance for this feature + var cutDistance = ComputeCutDistance(codes); + + var ctx = new FeatureContext + { + Codes = codes, + FeatureNumber = featureNumber, + PartName = partName, + IsFirstFeatureOfPart = isFirstFeatureOfPart, + IsLastFeatureOnSheet = isLastFeature, + IsSafetyHeadraise = isSafetyHeadraise, + IsExteriorFeature = false, + LibraryFile = libraryFile, + CutDistance = cutDistance, + SheetDiagonal = sheetDiagonal + }; + + _featureWriter.Write(w, ctx); + lastPartName = partName; + } + + // 6. Footer + w.WriteLine("M42"); + w.WriteLine("G0X0Y0"); + if (_config.PalletExchange != PalletMode.None) + w.WriteLine($"N{sheetIndex + 1}M50"); + w.WriteLine($"M99(END OF {nestName}.{sheetIndex:D3})"); + } + + private static double ComputeCutDistance(List codes) + { + var distance = 0.0; + var currentPos = Vector.Zero; + + foreach (var code in codes) + { + if (code is RapidMove rapid) + { + currentPos = rapid.EndPoint; + } + else if (code is LinearMove linear) + { + distance += currentPos.DistanceTo(linear.EndPoint); + currentPos = linear.EndPoint; + } + else if (code is ArcMove arc) + { + distance += currentPos.DistanceTo(arc.EndPoint); + currentPos = arc.EndPoint; + } + } + + return distance; + } +} diff --git a/OpenNest.Tests/Cincinnati/CincinnatiSheetWriterTests.cs b/OpenNest.Tests/Cincinnati/CincinnatiSheetWriterTests.cs new file mode 100644 index 0000000..a74b718 --- /dev/null +++ b/OpenNest.Tests/Cincinnati/CincinnatiSheetWriterTests.cs @@ -0,0 +1,117 @@ +using System.IO; +using System.Linq; +using System.Text; +using OpenNest.CNC; +using OpenNest.Geometry; +using OpenNest.Posts.Cincinnati; + +namespace OpenNest.Tests.Cincinnati; + +public class CincinnatiSheetWriterTests +{ + [Fact] + public void WriteSheet_EmitsSheetHeader() + { + var config = new CincinnatiPostConfig + { + DefaultLibraryFile = "MS135N2PANEL.lib", + PostedAccuracy = 4 + }; + var plate = new Plate(48.0, 96.0); + plate.Parts.Add(new Part(new Drawing("TestPart", CreateSimpleProgram()))); + + var sb = new StringBuilder(); + using var sw = new StringWriter(sb); + var sheetWriter = new CincinnatiSheetWriter(config, new ProgramVariableManager()); + + sheetWriter.Write(sw, plate, "TestNest", 1, 101); + + var output = sb.ToString(); + Assert.Contains(":101", output); + Assert.Contains("( Sheet 1 )", output); + Assert.Contains("#110=", output); + Assert.Contains("#111=", output); + Assert.Contains("G92X#5021Y#5022", output); + Assert.Contains("M99", output); + } + + [Fact] + public void WriteSheet_EmitsReturnToOriginAndPalletExchange() + { + var config = new CincinnatiPostConfig + { + PalletExchange = PalletMode.EndOfSheet, + PostedAccuracy = 4 + }; + var plate = new Plate(48.0, 96.0); + plate.Parts.Add(new Part(new Drawing("TestPart", CreateSimpleProgram()))); + + var sb = new StringBuilder(); + using var sw = new StringWriter(sb); + var sheetWriter = new CincinnatiSheetWriter(config, new ProgramVariableManager()); + + sheetWriter.Write(sw, plate, "TestNest", 1, 101); + + var output = sb.ToString(); + Assert.Contains("M42", output); + Assert.Contains("G0X0Y0", output); + Assert.Contains("M50", output); + } + + [Fact] + public void WriteSheet_SkipsEmptyPlate() + { + var config = new CincinnatiPostConfig { PostedAccuracy = 4 }; + var plate = new Plate(48.0, 96.0); + + var sb = new StringBuilder(); + using var sw = new StringWriter(sb); + var sheetWriter = new CincinnatiSheetWriter(config, new ProgramVariableManager()); + + sheetWriter.Write(sw, plate, "TestNest", 1, 101); + + Assert.Equal("", sb.ToString()); + } + + [Fact] + public void WriteSheet_SplitsMultiContourParts() + { + var config = new CincinnatiPostConfig { PostedAccuracy = 4 }; + var pgm = new Program(); + // First contour (hole) + pgm.Codes.Add(new RapidMove(1, 1)); + pgm.Codes.Add(new LinearMove(2, 1)); + pgm.Codes.Add(new LinearMove(2, 2)); + pgm.Codes.Add(new LinearMove(1, 1)); + // Second contour (exterior) + pgm.Codes.Add(new RapidMove(0, 0)); + pgm.Codes.Add(new LinearMove(5, 0)); + pgm.Codes.Add(new LinearMove(5, 5)); + pgm.Codes.Add(new LinearMove(0, 0)); + + var plate = new Plate(48.0, 96.0); + plate.Parts.Add(new Part(new Drawing("MultiContour", pgm))); + + var sb = new StringBuilder(); + using var sw = new StringWriter(sb); + var sheetWriter = new CincinnatiSheetWriter(config, new ProgramVariableManager()); + + sheetWriter.Write(sw, plate, "TestNest", 1, 101); + + var output = sb.ToString(); + // Should have two G84 pierce commands (one per contour) + var g84Count = output.Split('\n').Count(l => l.Trim() == "G84"); + Assert.Equal(2, g84Count); + } + + private static Program CreateSimpleProgram() + { + var pgm = new Program(); + pgm.Codes.Add(new RapidMove(0, 0)); + pgm.Codes.Add(new LinearMove(1, 0)); + pgm.Codes.Add(new LinearMove(1, 1)); + pgm.Codes.Add(new LinearMove(0, 1)); + pgm.Codes.Add(new LinearMove(0, 0)); + return pgm; + } +}