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