diff --git a/OpenNest.Console/Program.cs b/OpenNest.Console/Program.cs index 9438410..aecc222 100644 --- a/OpenNest.Console/Program.cs +++ b/OpenNest.Console/Program.cs @@ -669,8 +669,13 @@ static class NestConsole ); } + var outputFiles = post is IMultiFilePostProcessor multiFile + ? multiFile.GetOutputFiles(nest, outputFile) + : new[] { outputFile }; + post.Post(nest, outputFile); - Console.WriteLine($"Post: {post.Name} -> {outputFile}"); + foreach (var file in outputFiles) + Console.WriteLine($"Post: {post.Name} -> {file}"); } static void PrintUsage() diff --git a/OpenNest.Core/IMultiFilePostProcessor.cs b/OpenNest.Core/IMultiFilePostProcessor.cs new file mode 100644 index 0000000..4f7e7e9 --- /dev/null +++ b/OpenNest.Core/IMultiFilePostProcessor.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace OpenNest +{ + /// + /// A post-processor whose + /// can write more than one file (for example one program per sheet). + /// + public interface IMultiFilePostProcessor : IPostProcessor + { + /// + /// The files will write + /// for this nest and chosen path, in order, with the current settings. + /// + IReadOnlyList GetOutputFiles(Nest nest, string outputFile); + } +} diff --git a/OpenNest.Tests/CincinnatiCIFiber/CIFiberPostProcessorTests.cs b/OpenNest.Tests/CincinnatiCIFiber/CIFiberPostProcessorTests.cs index 41454c1..9c3eb43 100644 --- a/OpenNest.Tests/CincinnatiCIFiber/CIFiberPostProcessorTests.cs +++ b/OpenNest.Tests/CincinnatiCIFiber/CIFiberPostProcessorTests.cs @@ -342,4 +342,207 @@ public class CIFiberPostProcessorTests Assert.Equal(120.0, config.MaxTableX); Assert.Equal("SSN", config.DefaultMaterialCode); } + + private static Nest MakeMultiSheetNest(params (double Width, double Length)[] extraSheets) + { + var nest = MakeSquareWithHoleNest(); + var drawing = nest.Plates[0].Parts[0].BaseDrawing; + foreach (var (width, length) in extraSheets) + { + var plate = new Plate(width, length); + plate.Parts.Add(new Part(drawing, new Vector(3, 4))); + nest.Plates.Add(plate); + } + return nest; + } + + private static string NewTempDir() + { + var dir = Path.Combine(Path.GetTempPath(), $"cifiber-{Guid.NewGuid():N}"); + Directory.CreateDirectory(dir); + return dir; + } + + [Fact] + public void Post_OneProgramPerSheet_WritesNumberedFiles_EachAFullProgram() + { + var nest = MakeMultiSheetNest((48, 96)); + nest.Plates.Add(new Plate(60, 120)); // empty sheet: not posted + var dir = NewTempDir(); + try + { + var chosen = Path.Combine(dir, "JOB.cnc"); + var post = new CIFiberPostProcessor(MakeConfig()); + + var expected = post.GetOutputFiles(nest, chosen); + post.Post(nest, chosen); + + Assert.Equal( + new[] { Path.Combine(dir, "JOB-1.cnc"), Path.Combine(dir, "JOB-2.cnc") }, + expected + ); + Assert.Equal( + expected.OrderBy(f => f), + Directory.GetFiles(dir).OrderBy(f => f) + ); + + var first = Lines(File.ReadAllText(expected[0])); + var second = Lines(File.ReadAllText(expected[1])); + + Assert.Contains("V.E.X_SIZE = 120.000", first); + Assert.Contains("V.E.X_SIZE = 96.000", second); + Assert.Contains("V.E.Y_SIZE = 48.000", second); + Assert.Contains("( Sheet number - 1 )", first); + Assert.Contains("( Sheet number - 2 )", second); + + foreach (var program in new[] { first, second }) + { + // Restart labels start over in every program. + Assert.Contains("N1:", program); + Assert.DoesNotContain("N3:", program); + Assert.Single(program, "M50"); + Assert.Equal( + new[] { "L PROGRAMEND.NC", "M50", "M30", "%", "" }, + program.TakeLast(5) + ); + } + Assert.Contains("G0X3Y3.5", second); // hole pierce at the sheet-2 part location + } + finally + { + Directory.Delete(dir, true); + } + } + + [Fact] + public void Post_SingleProgram_ChangesPalletBetweenSheets() + { + var nest = MakeMultiSheetNest((60, 120), (60, 120)); + var config = MakeConfig(); + config.OneProgramPerSheet = false; + var dir = NewTempDir(); + try + { + var chosen = Path.Combine(dir, "JOB.cnc"); + var post = new CIFiberPostProcessor(config); + + Assert.Equal(new[] { chosen }, post.GetOutputFiles(nest, chosen)); + post.Post(nest, chosen); + Assert.Equal(new[] { chosen }, Directory.GetFiles(dir)); + + var lines = Lines(File.ReadAllText(chosen)); + Assert.Equal(3, lines.Count(l => l == "M50")); + Assert.Single(lines, "M30"); + + var sheet2 = lines.IndexOf("( Sheet number - 2 )"); + Assert.Equal( + new[] { "( PART END )", "/L \"L0\"", "M50", "( Sheet number - 2 )" }, + lines.Skip(sheet2 - 3).Take(4) + ); + Assert.True(lines.IndexOf("( Sheet number - 3 )") > sheet2); + + // Restart labels continue across sheets within one program. + Assert.Contains("N6:", lines); + Assert.Contains("V.E.R3=6", lines); + } + finally + { + Directory.Delete(dir, true); + } + } + + [Fact] + public void Post_SingleProgram_RejectsMixedSheetSizes_WritingNothing() + { + var nest = MakeMultiSheetNest((48, 96)); + var config = MakeConfig(); + config.OneProgramPerSheet = false; + var dir = NewTempDir(); + try + { + var ex = Assert.Throws(() => + new CIFiberPostProcessor(config).Post(nest, Path.Combine(dir, "JOB.cnc")) + ); + Assert.Contains("One program per sheet", ex.Message); + Assert.Empty(Directory.GetFiles(dir)); + } + finally + { + Directory.Delete(dir, true); + } + } + + [Fact] + public void Post_OneProgramPerSheet_OversizeSheet_WritesNoFiles() + { + var nest = MakeMultiSheetNest((60, 200)); // second sheet 200 long > 160.25 table + var dir = NewTempDir(); + try + { + var ex = Assert.Throws(() => + new CIFiberPostProcessor(MakeConfig()).Post(nest, Path.Combine(dir, "JOB.cnc")) + ); + Assert.Contains("exceeds maximum table X", ex.Message); + Assert.Empty(Directory.GetFiles(dir)); + } + finally + { + Directory.Delete(dir, true); + } + } + + [Fact] + public void Post_OneProgramPerSheet_SingleSheet_UsesChosenName() + { + var nest = MakeSquareWithHoleNest(); + var dir = NewTempDir(); + try + { + var chosen = Path.Combine(dir, "JOB.cnc"); + var post = new CIFiberPostProcessor(MakeConfig()); + + Assert.Equal(new[] { chosen }, post.GetOutputFiles(nest, chosen)); + post.Post(nest, chosen); + + Assert.Equal(Post(nest), File.ReadAllText(chosen)); + } + finally + { + Directory.Delete(dir, true); + } + } + + [Fact] + public void Post_PalletChangeCode_IsConfigurable() + { + var custom = MakeConfig(); + custom.PalletChangeCode = " M51 "; + var lines = Lines(Post(MakeSquareWithHoleNest(), custom)); + Assert.Equal(new[] { "L PROGRAMEND.NC", "M51", "M30", "%", "" }, lines.TakeLast(5)); + Assert.DoesNotContain("M50", lines); + + var none = MakeConfig(); + none.PalletChangeCode = ""; + lines = Lines(Post(MakeSquareWithHoleNest(), none)); + Assert.Equal(new[] { "L PROGRAMEND.NC", "M30", "%", "" }, lines.TakeLast(4)); + } + + [Fact] + public void PostToStream_MultiSheetPerSheetMode_Throws() + { + var nest = MakeMultiSheetNest((60, 120)); + var ex = Assert.Throws(() => Post(nest)); + Assert.Contains("file path", ex.Message); + } + + [Fact] + public void SavedConfigWithoutSheetSettings_DefaultsToPerSheetAndM50() + { + var config = System.Text.Json.JsonSerializer.Deserialize( + """{ "MaxTableX": 120.0 }""" + )!; + + Assert.True(config.OneProgramPerSheet); + Assert.Equal("M50", config.PalletChangeCode); + } } diff --git a/OpenNest.Tests/PostSettings/PostSettingsLayoutTests.cs b/OpenNest.Tests/PostSettings/PostSettingsLayoutTests.cs index 670287d..d129e40 100644 --- a/OpenNest.Tests/PostSettings/PostSettingsLayoutTests.cs +++ b/OpenNest.Tests/PostSettings/PostSettingsLayoutTests.cs @@ -17,7 +17,7 @@ public class PostSettingsLayoutTests Assert.NotNull(sections); Assert.Equal( - new[] { "Machine", "Material", "Program output", "Macros" }, + new[] { "Machine", "Sheets", "Material", "Program output", "Macros" }, sections.Select(s => s.Name) ); Assert.All(sections, s => Assert.NotEmpty(s.Description)); @@ -40,7 +40,11 @@ public class PostSettingsLayoutTests Assert.Equal("Maximum table X", machine[1].Label); Assert.Contains("0 disables", machine[1].Description); - var materials = sections[1].Fields[0]; + var sheets = sections[1].Fields; + Assert.Equal(new[] { "OneProgramPerSheet", "PalletChangeCode" }, sheets.Select(f => f.Name)); + Assert.Equal(PostSettingKind.Boolean, sheets[0].Kind); + + var materials = sections[2].Fields[0]; Assert.Equal(PostSettingKind.StringMap, materials.Kind); Assert.Equal("Material name", materials.KeyHeader); Assert.Equal("Machine code", materials.ValueHeader); diff --git a/OpenNest/Forms/MainForm.cs b/OpenNest/Forms/MainForm.cs index 46da9c4..58028e9 100644 --- a/OpenNest/Forms/MainForm.cs +++ b/OpenNest/Forms/MainForm.cs @@ -1447,10 +1447,71 @@ namespace OpenNest.Forms if (dialog.ShowDialog() == DialogResult.OK) { var path = dialog.FileName; + + if (postProcessor is IMultiFilePostProcessor multiFile) + { + var files = multiFile.GetOutputFiles(activeForm.Nest, path); + if (!ConfirmOverwrite(files, path)) + return; + + try + { + postProcessor.Post(activeForm.Nest, path); + } + catch (Exception ex) + when (ex is InvalidOperationException or IOException or UnauthorizedAccessException) + { + MessageBox.Show( + this, + ex.Message, + postProcessor.Name, + MessageBoxButtons.OK, + MessageBoxIcon.Warning + ); + return; + } + + if (files.Count > 1) + MessageBox.Show( + this, + $"Saved {files.Count} programs, one per sheet:\n\n" + + string.Join("\n", files.Select(Path.GetFileName)) + + $"\n\nin {Path.GetDirectoryName(path)}", + postProcessor.Name, + MessageBoxButtons.OK, + MessageBoxIcon.Information + ); + return; + } + postProcessor.Post(activeForm.Nest, path); } } + /// + /// The save dialog only checks the chosen name; ask before replacing any + /// other existing file a multi-file post will write. + /// + private bool ConfirmOverwrite(IReadOnlyList files, string chosenPath) + { + var existing = files + .Where(f => File.Exists(f) && !string.Equals(f, chosenPath, StringComparison.OrdinalIgnoreCase)) + .ToList(); + if (existing.Count == 0) + return true; + + var answer = MessageBox.Show( + this, + "These files already exist and will be replaced:\n\n" + + string.Join("\n", existing.Select(Path.GetFileName)) + + "\n\nReplace them?", + "Confirm Save", + MessageBoxButtons.YesNo, + MessageBoxIcon.Warning + ); + return answer == DialogResult.Yes; + } + private void CalculateNestCutTime_Click(object sender, EventArgs e) { if (activeForm == null) diff --git a/Posts/OpenNest.Posts.CincinnatiCIFiber/CIFiberPostConfig.cs b/Posts/OpenNest.Posts.CincinnatiCIFiber/CIFiberPostConfig.cs index 452acfe..1a4586a 100644 --- a/Posts/OpenNest.Posts.CincinnatiCIFiber/CIFiberPostConfig.cs +++ b/Posts/OpenNest.Posts.CincinnatiCIFiber/CIFiberPostConfig.cs @@ -16,23 +16,29 @@ namespace OpenNest.Posts.CincinnatiCIFiber Description = "Which CI Fiber table this configuration drives and the largest sheet it accepts." )] [PostSettingsSection( - MaterialSection, + SheetsSection, 1, + Description = "How a nest with more than one sheet is written, and the code that swaps pallets." + )] + [PostSettingsSection( + MaterialSection, + 2, Description = "Maps OpenNest material names to the controller's V.E.MATERIAL code." )] [PostSettingsSection( OutputSection, - 2, + 3, Description = "Number format, units, and what the posted program includes." )] [PostSettingsSection( MacrosSection, - 3, + 4, Description = "Controller subroutines called around each contour and at program start and end. They must exist on the machine." )] public class CIFiberPostConfig { private const string MachineSection = "Machine"; + private const string SheetsSection = "Sheets"; private const string MaterialSection = "Material"; private const string OutputSection = "Program output"; private const string MacrosSection = "Macros"; @@ -91,6 +97,35 @@ namespace OpenNest.Posts.CincinnatiCIFiber [PostSetting(MachineSection, 2, Minimum = 0, Maximum = 10000, DecimalPlaces = 3)] public double MaxTableY { get; set; } = 81.25; + /// + /// Write each sheet as its own program (NAME-1.cnc, NAME-2.cnc, ...), the + /// Cincinnati convention for batch runs. When false, every sheet goes into + /// one program with a pallet change between sheets. A nest with a single + /// sheet always posts to the chosen file name. + /// + [DisplayName("One program per sheet")] + [Description( + "Save each sheet as its own program: JOB.cnc becomes JOB-1.cnc, JOB-2.cnc, ... " + + "Clear to put every sheet in one program, with a pallet change between sheets. " + + "A single-sheet nest always saves to the chosen name." + )] + [PostSetting(SheetsSection, 0)] + public bool OneProgramPerSheet { get; set; } = true; + + /// + /// Line written to swap pallets: after the last sheet, and between sheets + /// in a single program. M50 comes from the machine sample and the CL-series + /// manual (EM-423 §3.50); it is unconfirmed for multi-sheet CI Fiber runs. + /// Blank writes no pallet change. + /// + [DisplayName("Pallet change code")] + [Description( + "Written after each sheet to swap pallets. M50 matches the machine sample; " + + "confirm it for multi-sheet runs. Blank writes no pallet change." + )] + [PostSetting(SheetsSection, 1)] + public string PalletChangeCode { get; set; } = "M50"; + /// Skippable global subroutine used to cancel comp / park between features. [DisplayName("Layer: cancel (L0)")] [Description("Called before each contour and at the end to cancel compensation and park.")] diff --git a/Posts/OpenNest.Posts.CincinnatiCIFiber/CIFiberPostProcessor.cs b/Posts/OpenNest.Posts.CincinnatiCIFiber/CIFiberPostProcessor.cs index 99e4d28..bf6bf8b 100644 --- a/Posts/OpenNest.Posts.CincinnatiCIFiber/CIFiberPostProcessor.cs +++ b/Posts/OpenNest.Posts.CincinnatiCIFiber/CIFiberPostProcessor.cs @@ -16,7 +16,9 @@ namespace OpenNest.Posts.CincinnatiCIFiber /// the configured table envelope. Emits the TF5200 skippable-macro program /// structure of the machine sample (see ). /// - public sealed class CIFiberPostProcessor : IConfigurablePostProcessor + public sealed class CIFiberPostProcessor + : IConfigurablePostProcessor, + IMultiFilePostProcessor { private static readonly JsonSerializerOptions JsonOptions = new() { @@ -70,6 +72,11 @@ namespace OpenNest.Posts.CincinnatiCIFiber return Path.Combine(dir, name + ".json"); } + /// + /// Writes every sheet into one program. A stream holds a single program, + /// so this throws when "One program per sheet" is on and the nest has more + /// than one sheet; use for that. + /// public void Post(Nest nest, Stream outputStream) { if (nest == null) @@ -77,26 +84,86 @@ namespace OpenNest.Posts.CincinnatiCIFiber if (outputStream == null) throw new ArgumentNullException(nameof(outputStream)); - // CRLF file, ASCII (UTF-8 without BOM), matching the machine sample. - var encoding = new UTF8Encoding(false); - using var writer = new StreamWriter( - outputStream, - encoding, - 1024, - leaveOpen: true - ); - new CIFiberProgramWriter(Config).Write(nest, writer); - writer.Flush(); + if (SplitsSheets(nest)) + throw new InvalidOperationException( + "\"One program per sheet\" writes several files; post this nest to a file path." + ); + + var program = Render(w => new CIFiberProgramWriter(Config).Write(nest, w)); + outputStream.Write(program, 0, program.Length); } + /// + /// Writes the program(s) for the nest. With "One program per sheet" and more + /// than one sheet, JOB.cnc becomes JOB-1.cnc, JOB-2.cnc, ...; + /// otherwise the single program goes to . All + /// programs are generated before any file is written, so a validation + /// failure leaves no partial output. + /// public void Post(Nest nest, string outputFile) { - using var fs = new FileStream( - outputFile, - FileMode.Create, - FileAccess.Write - ); - Post(nest, fs); + if (nest == null) + throw new ArgumentNullException(nameof(nest)); + if (string.IsNullOrEmpty(outputFile)) + throw new ArgumentNullException(nameof(outputFile)); + + var files = GetOutputFiles(nest, outputFile); + var programs = new List(); + + if (files.Count == 1) + { + programs.Add(Render(w => new CIFiberProgramWriter(Config).Write(nest, w))); + } + else + { + var sheets = CIFiberProgramWriter.PostedSheets(nest); + var writer = new CIFiberProgramWriter(Config); + writer.Validate(sheets, singleProgram: false); + for (var i = 0; i < sheets.Count; i++) + { + var sheetNumber = i + 1; + var sheet = sheets[i]; + programs.Add(Render(w => writer.WriteSheet(nest, sheet, sheetNumber, w))); + } + } + + for (var i = 0; i < files.Count; i++) + File.WriteAllBytes(files[i], programs[i]); + } + + public IReadOnlyList GetOutputFiles(Nest nest, string outputFile) + { + if (nest == null) + throw new ArgumentNullException(nameof(nest)); + if (string.IsNullOrEmpty(outputFile)) + throw new ArgumentNullException(nameof(outputFile)); + + if (!SplitsSheets(nest)) + return new[] { outputFile }; + + var dir = Path.GetDirectoryName(outputFile) ?? ""; + var name = Path.GetFileNameWithoutExtension(outputFile); + var ext = Path.GetExtension(outputFile); + var count = CIFiberProgramWriter.PostedSheets(nest).Count; + + return Enumerable + .Range(1, count) + .Select(n => Path.Combine(dir, $"{name}-{n}{ext}")) + .ToList(); + } + + private bool SplitsSheets(Nest nest) => + Config.OneProgramPerSheet && CIFiberProgramWriter.PostedSheets(nest).Count > 1; + + // CRLF file, ASCII (UTF-8 without BOM), matching the machine sample. + private static byte[] Render(Action write) + { + using var ms = new MemoryStream(); + using (var writer = new StreamWriter(ms, new UTF8Encoding(false), 1024, leaveOpen: true)) + { + write(writer); + } + return ms.ToArray(); } } } diff --git a/Posts/OpenNest.Posts.CincinnatiCIFiber/CIFiberProgramWriter.cs b/Posts/OpenNest.Posts.CincinnatiCIFiber/CIFiberProgramWriter.cs index 64c680e..a51120f 100644 --- a/Posts/OpenNest.Posts.CincinnatiCIFiber/CIFiberProgramWriter.cs +++ b/Posts/OpenNest.Posts.CincinnatiCIFiber/CIFiberProgramWriter.cs @@ -26,6 +26,7 @@ namespace OpenNest.Posts.CincinnatiCIFiber /// $GOTO NP3: /// N0: /// ( Sheet number - 1 ) + /// ... (single-program mode only: /L "L0", pallet change, next sheet) /// ( Part #k ) ( PART:... ) V.E.R4=k /// N<n>: /L "L0" V.E.R3=<n> G0X..Y.. /// /L "L2" + G41 | /L "L4" + G42 @@ -36,7 +37,7 @@ namespace OpenNest.Posts.CincinnatiCIFiber /// ( PART END ) /// /L "L0" /// L PROGRAMEND.NC - /// M50 + /// M50 (configurable pallet change) /// M30 /// % /// @@ -57,14 +58,66 @@ namespace OpenNest.Posts.CincinnatiCIFiber _fmt = new CIFiberFormatter(config.PostedAccuracy); } + /// The plates that are posted: every plate with parts, in nest order. + public static IReadOnlyList PostedSheets(Nest nest) => + nest?.Plates.Where(p => p.Parts.Count > 0).ToList() + ?? throw new ArgumentNullException(nameof(nest)); + + /// + /// Writes every posted sheet into one program, with a pallet change + /// between sheets and after the last. + /// public void Write(Nest nest, TextWriter w) { if (nest == null) throw new ArgumentNullException(nameof(nest)); - var plates = nest.Plates.Where(p => p.Parts.Count > 0).ToList(); + Write(nest, PostedSheets(nest), 1, w); + } - WriteHeader(nest, w); + /// + /// Writes one sheet as a complete program. Contour labels restart at 1; + /// only labels the sheet comment. + /// + public void WriteSheet(Nest nest, Plate sheet, int sheetNumber, TextWriter w) + { + if (nest == null) + throw new ArgumentNullException(nameof(nest)); + if (sheet == null) + throw new ArgumentNullException(nameof(sheet)); + + Write(nest, new[] { sheet }, sheetNumber, w); + } + + /// + /// Throws if any sheet exceeds the table, or if a single program would hold + /// sheets of different sizes (its header carries only one size). + /// + public void Validate(IReadOnlyList sheets, bool singleProgram) + { + foreach (var sheet in sheets) + _config.ValidateTableSize(sheet.Size.Length, sheet.Size.Width); + + if (!singleProgram || sheets.Count < 2) + return; + + var first = sheets[0].Size; + if (sheets.Any(p => !SameSize(p.Size, first))) + throw new InvalidOperationException( + "The sheets are different sizes, but one program has a single sheet " + + "size in its header. Turn on \"One program per sheet\" to post them." + ); + } + + private static bool SameSize(Size a, Size b) => + System.Math.Abs(a.Length - b.Length) < 1e-6 && System.Math.Abs(a.Width - b.Width) < 1e-6; + + private void Write(Nest nest, IReadOnlyList sheets, int firstSheetNumber, TextWriter w) + { + // Check every sheet before writing anything. + Validate(sheets, singleProgram: true); + + WriteHeader(nest, sheets.Count > 0 ? sheets[0] : nest.Plates.FirstOrDefault(), w); CIFiberFormatter.Line(w, "G90"); CIFiberFormatter.Line(w, $"L {_config.ProgramStartMacro}"); @@ -73,19 +126,23 @@ namespace OpenNest.Posts.CincinnatiCIFiber CIFiberFormatter.Line(w, "N0:"); var contourNumber = 0; - for (var s = 0; s < plates.Count; s++) + for (var s = 0; s < sheets.Count; s++) { - var plate = plates[s]; - _config.ValidateTableSize(plate.Size.Length, plate.Size.Width); + if (s > 0) + { + // Park, then swap pallets before the next sheet. + CIFiberFormatter.Line(w, SkippableLine(_config.LayerCancel)); + WritePalletChange(w); + } - CIFiberFormatter.Line(w, $"( Sheet number - {s + 1} )"); - contourNumber = WriteSheet(plate, w, contourNumber); + CIFiberFormatter.Line(w, $"( Sheet number - {firstSheetNumber + s} )"); + contourNumber = WriteSheet(sheets[s], w, contourNumber); } WriteTail(w); } - private void WriteHeader(Nest nest, TextWriter w) + private void WriteHeader(Nest nest, Plate firstSheet, TextWriter w) { CIFiberFormatter.Line(w, $"( {nest.Name ?? ""} )"); CIFiberFormatter.Line(w, $"( CONFIGURATION - {_config.ConfigurationName} )"); @@ -106,12 +163,10 @@ namespace OpenNest.Posts.CincinnatiCIFiber CIFiberFormatter.Line(w, $"V.E.MATERIAL = \"{code}\""); CIFiberFormatter.Line(w, $"V.E.THICKNESS = {_fmt.Fixed(nest.Thickness)}"); - // Multi-plate nests run one sheet after another in this program; - // the header size describes the first sheet (the machine sample - // carries a single sheet). - var firstPlate = nest.Plates.FirstOrDefault(); - var xSize = firstPlate?.Size.Length ?? 0.0; - var ySize = firstPlate?.Size.Width ?? 0.0; + // A single program holding several sheets still carries only the + // first sheet's size (the machine sample has one sheet). + var xSize = firstSheet?.Size.Length ?? 0.0; + var ySize = firstSheet?.Size.Width ?? 0.0; CIFiberFormatter.Line(w, $"V.E.X_SIZE = {_fmt.Fixed(xSize)}"); CIFiberFormatter.Line(w, $"V.E.Y_SIZE = {_fmt.Fixed(ySize)}"); @@ -128,11 +183,17 @@ namespace OpenNest.Posts.CincinnatiCIFiber { CIFiberFormatter.Line(w, SkippableLine(_config.LayerCancel)); CIFiberFormatter.Line(w, $"L {_config.ProgramEndMacro}"); - CIFiberFormatter.Line(w, "M50"); + WritePalletChange(w); CIFiberFormatter.Line(w, "M30"); CIFiberFormatter.Line(w, "%"); } + private void WritePalletChange(TextWriter w) + { + if (!string.IsNullOrWhiteSpace(_config.PalletChangeCode)) + CIFiberFormatter.Line(w, _config.PalletChangeCode.Trim()); + } + private int WriteSheet(Plate plate, TextWriter w, int contourNumber) { // Cut-offs run last: severing the sheet first would free the diff --git a/Posts/OpenNest.Posts.CincinnatiCIFiber/OpenNest.Posts.CincinnatiCIFiber.json b/Posts/OpenNest.Posts.CincinnatiCIFiber/OpenNest.Posts.CincinnatiCIFiber.json index 56077bf..5770b6b 100644 --- a/Posts/OpenNest.Posts.CincinnatiCIFiber/OpenNest.Posts.CincinnatiCIFiber.json +++ b/Posts/OpenNest.Posts.CincinnatiCIFiber/OpenNest.Posts.CincinnatiCIFiber.json @@ -7,6 +7,8 @@ "SkipScribe": true, "MaxTableX": 160.25, "MaxTableY": 81.25, + "OneProgramPerSheet": true, + "PalletChangeCode": "M50", "LayerCancel": "L0", "LayerInteriorLeadin": "L2", "LayerExteriorLeadin": "L4", diff --git a/docs/cincinnati-ci-fiber-post-output.md b/docs/cincinnati-ci-fiber-post-output.md index 710618c..12cfb48 100644 --- a/docs/cincinnati-ci-fiber-post-output.md +++ b/docs/cincinnati-ci-fiber-post-output.md @@ -19,11 +19,13 @@ uses the Cincinnati machine-sample convention, not a generic TF5200 laser API: `V.E.X_SIZE`, `V.E.Y_SIZE`, and `V.E.UNIT`. Sheet weight is omitted by default. - Startup: `G90`, `L PROGRAMSTART.NC`, then `P3=V.E.R3`, `$GOTO NP3:`, and `N0:`. - Parts follow plate order. `V.E.R4` identifies the part within its sheet; - numbered contour labels and `V.E.R3` continue across sheets for restart lookup. + numbered contour labels and `V.E.R3` count up through the program for restart + lookup (they restart at 1 in each per-sheet program). - Each contour: `/L "L0"`, restart number, rapid to pierce, then interior `/L "L2"` with `G41` or exterior `/L "L4"` with `G42`; linear lead-in, `/L "L6"`, cutting moves, and `/L "ZHSOFF"`. -- Tail: `/L "L0"`, `L PROGRAMEND.NC`, `M50`, `M30`, and `%`. +- Tail: `/L "L0"`, `L PROGRAMEND.NC`, the pallet-change code (`M50`), `M30`, + and `%`. - Motion endpoints are sheet-absolute XY. Arc `I`/`J` are offsets from the arc start, matching the G162 convention; the post does not explicitly emit G162. - Sheet cut-offs post after every part on their sheet. Each segment is an open @@ -42,9 +44,9 @@ uses the Cincinnati machine-sample convention, not a generic TF5200 laser API: See [CIFiberPostConfig](../Posts/OpenNest.Posts.CincinnatiCIFiber/CIFiberPostConfig.cs) for macro names, material mappings, unit codes, precision, and table limits. The post is named for the machine family; table size belongs in configuration. -In the desktop app these settings open in a sectioned editor (Machine, Material, -Program output, Macros) with a material-code table; values are validated and -saved only when OK is pressed. +In the desktop app these settings open in a sectioned editor (Machine, Sheets, +Material, Program output, Macros) with a material-code table; values are +validated and saved only when OK is pressed. - Assign linear lead-ins before posting, including circular holes. The writer rejects missing or arc-first lead-ins. Its conservative rule cites the G238 @@ -56,19 +58,31 @@ saved only when OK is pressed. - `InchUnitCode` defaults to `1`; `MetricUnitCode` defaults to `0` but remains unconfirmed. Table limits compare directly with nest dimensions, without unit conversion. Do not assume changing the unit code establishes metric support. -- Multiple nonempty plates are written sequentially, but dimensional header - values come only from the first plate and the pallet-change tail occurs once. - This is not proof of a qualified multi-sheet machine cycle. -- Validation can throw after output has started. Discard any output from a failed - post; it may be incomplete. Successful generation and tests do not establish +- Plates without parts are not posted. With **One program per sheet** (the + default) and more than one sheet, `JOB.cnc` is written as `JOB-1.cnc`, + `JOB-2.cnc`, ..., each a complete program with its own sheet size and pallet + change. This follows the CL-series batch rule (EM-423 §7.4: one sheet per + program; the operator sets repeats). A single-sheet nest keeps the chosen name. + Plate quantity is not written; repeat the program at the machine. +- With the option off, every sheet goes into one program: `/L "L0"` and the pallet + change separate sheets, and the header carries the first sheet's size. Sheets + of different sizes are rejected in this mode. Posting to a stream always + produces one program, so it rejects a multi-sheet nest in per-sheet mode. +- **Release blocker:** `PalletChangeCode` defaults to `M50`, taken from the + single-sheet machine sample and the CL-series manual (EM-423 §3.50). Neither the + pallet code nor the between-sheet sequence has been confirmed for multi-sheet CI + Fiber runs. Confirm both against a Cincinnati multi-sheet sample before release. +- All programs are generated and every sheet is validated before any file is + written, so a failed file post writes nothing. A failed stream post may leave + partial output; discard it. Successful generation and tests do not establish that a program is safe to run on a particular machine. ## Verification [Output-contract tests](../OpenNest.Tests/CincinnatiCIFiber/CIFiberPostProcessorTests.cs) cover a square with a hole, coordinate transforms/formatting, lead-in rejection, -cut-off ordering and output, -table bounds, and suppression/scribe handling. Run: +cut-off ordering and output, table bounds, suppression/scribe handling, and +multi-sheet output in both modes. Run: ```sh dotnet test OpenNest.Tests/OpenNest.Tests.csproj --filter FullyQualifiedName~CincinnatiCIFiber