feat: use Plate.Quantity as M98 L count for duplicate sheets in Cincinnati post

Instead of emitting separate M98 calls per identical sheet, use the L
(loop count) parameter so the operator can adjust quantity at the control.
M50 pallet exchange moves inside the sheet subprogram so each L iteration
gets its own exchange cycle. GOTO targets now correspond to layout groups.
Also fixes sheet name comment outputting dimensions in wrong order.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-02 11:52:34 -04:00
parent f26edb824d
commit ec0baad585
6 changed files with 86 additions and 60 deletions

View File

@@ -103,21 +103,20 @@ namespace OpenNest.Posts.Cincinnati
using var writer = new StreamWriter(outputStream, Encoding.UTF8, 1024, leaveOpen: true); using var writer = new StreamWriter(outputStream, Encoding.UTF8, 1024, leaveOpen: true);
// Main program // Main program
preamble.WriteMainProgram(writer, nest.Name ?? "NEST", materialDesc, plates.Count, initialCutLibrary); preamble.WriteMainProgram(writer, nest.Name ?? "NEST", materialDesc, plates, initialCutLibrary);
// Variable declaration subprogram // Variable declaration subprogram
preamble.WriteVariableDeclaration(writer, vars); preamble.WriteVariableDeclaration(writer, vars);
// Sheet subprograms // Sheet subprograms (one per unique layout, quantity handled via L count in main)
for (var i = 0; i < plates.Count; i++) for (var i = 0; i < plates.Count; i++)
{ {
var plate = plates[i]; var plate = plates[i];
var sheetIndex = i + 1; var layoutIndex = i + 1;
var subNumber = Config.SheetSubprogramStart + i; var subNumber = Config.SheetSubprogramStart + i;
var cutLibrary = resolver.ResolveCutLibrary(nest.Material?.Name ?? "", nest.Thickness, gas); var cutLibrary = resolver.ResolveCutLibrary(nest.Material?.Name ?? "", nest.Thickness, gas);
var isLastSheet = i == plates.Count - 1; sheetWriter.Write(writer, plate, nest.Name ?? "NEST", layoutIndex, subNumber,
sheetWriter.Write(writer, plate, nest.Name ?? "NEST", sheetIndex, subNumber, cutLibrary, etchLibrary, partSubprograms, userVarMapping);
cutLibrary, etchLibrary, partSubprograms, isLastSheet, userVarMapping);
} }
// Part sub-programs (if enabled) // Part sub-programs (if enabled)

View File

@@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using OpenNest; using OpenNest;
using OpenNest.CNC; using OpenNest.CNC;
@@ -23,7 +24,7 @@ public sealed class CincinnatiPreambleWriter
/// </summary> /// </summary>
/// <param name="initialLibrary">Resolved G89 library file for the initial process setup.</param> /// <param name="initialLibrary">Resolved G89 library file for the initial process setup.</param>
public void WriteMainProgram(TextWriter w, string nestName, string materialDescription, public void WriteMainProgram(TextWriter w, string nestName, string materialDescription,
int sheetCount, string initialLibrary) List<Plate> plates, string initialLibrary)
{ {
w.WriteLine(CoordinateFormatter.Comment($"NEST {nestName}")); w.WriteLine(CoordinateFormatter.Comment($"NEST {nestName}"));
w.WriteLine(CoordinateFormatter.Comment($"CONFIGURATION - {_config.ConfigurationName}")); w.WriteLine(CoordinateFormatter.Comment($"CONFIGURATION - {_config.ConfigurationName}"));
@@ -54,10 +55,16 @@ public sealed class CincinnatiPreambleWriter
w.WriteLine("GOTO1 (GOTO SHEET NUMBER)"); w.WriteLine("GOTO1 (GOTO SHEET NUMBER)");
for (var i = 1; i <= sheetCount; i++) for (var i = 0; i < plates.Count; i++)
{ {
var subNum = _config.SheetSubprogramStart + (i - 1); var layoutNumber = i + 1;
w.WriteLine($"N{i} M98 P{subNum} (SHEET {i})"); var subNum = _config.SheetSubprogramStart + i;
var qty = System.Math.Max(plates[i].Quantity, 1);
var lParam = qty > 1 ? $" L{qty}" : "";
var sheetLabel = qty > 1
? $"LAYOUT {layoutNumber} - {qty} SHEETS"
: $"LAYOUT {layoutNumber}";
w.WriteLine($"N{layoutNumber} M98 P{subNum}{lParam} ({sheetLabel})");
} }
w.WriteLine("M42"); w.WriteLine("M42");

View File

@@ -35,10 +35,9 @@ public sealed class CincinnatiSheetWriter
/// Optional mapping of (drawingId, rotationKey) to sub-program number. /// Optional mapping of (drawingId, rotationKey) to sub-program number.
/// When provided, non-cutoff parts are emitted as M98 calls instead of inline features. /// When provided, non-cutoff parts are emitted as M98 calls instead of inline features.
/// </param> /// </param>
public void Write(TextWriter w, Plate plate, string nestName, int sheetIndex, int subNumber, public void Write(TextWriter w, Plate plate, string nestName, int layoutIndex, int subNumber,
string cutLibrary, string etchLibrary, string cutLibrary, string etchLibrary,
Dictionary<(int, long), int> partSubprograms = null, Dictionary<(int, long), int> partSubprograms = null,
bool isLastSheet = false,
Dictionary<(int drawingId, string varName), int> userVarMapping = null) Dictionary<(int drawingId, string varName), int> userVarMapping = null)
{ {
if (plate.Parts.Count == 0) if (plate.Parts.Count == 0)
@@ -52,11 +51,10 @@ public sealed class CincinnatiSheetWriter
// 1. Sheet header // 1. Sheet header
w.WriteLine("(*****************************************************)"); w.WriteLine("(*****************************************************)");
w.WriteLine($"( START OF {nestName}.{sheetIndex:D3} )"); w.WriteLine($"( START OF {nestName}.{layoutIndex:D3} )");
w.WriteLine($":{subNumber}"); w.WriteLine($":{subNumber}");
w.WriteLine($"( Sheet {sheetIndex} )"); w.WriteLine($"( Layout {layoutIndex} )");
w.WriteLine($"( Layout {sheetIndex} )"); w.WriteLine($"( SHEET NAME = {_fmt.FormatCoord(width)} X {_fmt.FormatCoord(length)} )");
w.WriteLine($"( SHEET NAME = {_fmt.FormatCoord(length)} X {_fmt.FormatCoord(width)} )");
w.WriteLine($"( Total parts on sheet = {partCount} )"); w.WriteLine($"( Total parts on sheet = {partCount} )");
w.WriteLine($"#{_config.SheetWidthVariable}={_fmt.FormatCoord(width)} (SHEET WIDTH FOR CUTOFFS)"); w.WriteLine($"#{_config.SheetWidthVariable}={_fmt.FormatCoord(width)} (SHEET WIDTH FOR CUTOFFS)");
w.WriteLine($"#{_config.SheetLengthVariable}={_fmt.FormatCoord(length)} (SHEET LENGTH FOR CUTOFFS)"); w.WriteLine($"#{_config.SheetLengthVariable}={_fmt.FormatCoord(length)} (SHEET LENGTH FOR CUTOFFS)");
@@ -95,11 +93,9 @@ public sealed class CincinnatiSheetWriter
// 5. Footer // 5. Footer
w.WriteLine("M42"); w.WriteLine("M42");
var emitM50 = _config.PalletExchange == PalletMode.EndOfSheet if (_config.PalletExchange != PalletMode.None)
|| (_config.PalletExchange == PalletMode.StartAndEnd && isLastSheet); w.WriteLine("M50");
if (emitM50) w.WriteLine($"M99 (END OF {nestName}.{layoutIndex:D3})");
w.WriteLine($"N{sheetIndex + 1} M50");
w.WriteLine($"M99 (END OF {nestName}.{sheetIndex:D3})");
} }
private void WritePartsWithSubprograms(TextWriter w, List<Part> allParts, private void WritePartsWithSubprograms(TextWriter w, List<Part> allParts,

View File

@@ -38,7 +38,7 @@ public class CincinnatiPostProcessorTests
// Sheet subprogram // Sheet subprogram
Assert.Contains(":101", output); Assert.Contains(":101", output);
Assert.Contains("( Sheet 1 )", output); Assert.Contains("( Layout 1 )", output);
Assert.Contains("G84", output); Assert.Contains("G84", output);
Assert.Contains("M99", output); Assert.Contains("M99", output);
} }
@@ -150,8 +150,8 @@ public class CincinnatiPostProcessorTests
var output = Encoding.UTF8.GetString(ms.ToArray()); var output = Encoding.UTF8.GetString(ms.ToArray());
// Should only have one sheet subprogram call in main // Should only have one sheet subprogram call in main
Assert.Contains("N1 M98 P101 (SHEET 1)", output); Assert.Contains("N1 M98 P101 (LAYOUT 1)", output);
Assert.DoesNotContain("SHEET 2", output); Assert.DoesNotContain("LAYOUT 2", output);
} }
[Fact] [Fact]

View File

@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Text; using System.Text;
using OpenNest.CNC; using OpenNest.CNC;
@@ -19,7 +20,8 @@ public class CincinnatiPreambleWriterTests
using var sw = new StringWriter(sb); using var sw = new StringWriter(sb);
var writer = new CincinnatiPreambleWriter(config); var writer = new CincinnatiPreambleWriter(config);
writer.WriteMainProgram(sw, "TestNest", "Mild Steel, 10GA", 2, "MS135N2PANEL.lib"); var plates = new List<Plate> { new(48, 96), new(48, 96) };
writer.WriteMainProgram(sw, "TestNest", "Mild Steel, 10GA", plates, "MS135N2PANEL.lib");
var output = sb.ToString(); var output = sb.ToString();
Assert.Contains("( NEST TestNest )", output); Assert.Contains("( NEST TestNest )", output);
@@ -29,8 +31,8 @@ public class CincinnatiPreambleWriterTests
Assert.Contains("G89 PMS135N2PANEL.lib", output); Assert.Contains("G89 PMS135N2PANEL.lib", output);
Assert.Contains("M98 P100 (Variable Declaration)", output); Assert.Contains("M98 P100 (Variable Declaration)", output);
Assert.Contains("GOTO1 (GOTO SHEET NUMBER)", output); Assert.Contains("GOTO1 (GOTO SHEET NUMBER)", output);
Assert.Contains("N1 M98 P101 (SHEET 1)", output); Assert.Contains("N1 M98 P101 (LAYOUT 1)", output);
Assert.Contains("N2 M98 P102 (SHEET 2)", output); Assert.Contains("N2 M98 P102 (LAYOUT 2)", output);
Assert.Contains("M30 (END OF MAIN)", output); Assert.Contains("M30 (END OF MAIN)", output);
} }
@@ -42,7 +44,7 @@ public class CincinnatiPreambleWriterTests
using var sw = new StringWriter(sb); using var sw = new StringWriter(sb);
var writer = new CincinnatiPreambleWriter(config); var writer = new CincinnatiPreambleWriter(config);
writer.WriteMainProgram(sw, "Test", "", 1, ""); writer.WriteMainProgram(sw, "Test", "", new List<Plate> { new(48, 96) }, "");
Assert.Contains("G21 G90", sb.ToString()); Assert.Contains("G21 G90", sb.ToString());
} }
@@ -55,7 +57,7 @@ public class CincinnatiPreambleWriterTests
using var sw = new StringWriter(sb); using var sw = new StringWriter(sb);
var writer = new CincinnatiPreambleWriter(config); var writer = new CincinnatiPreambleWriter(config);
writer.WriteMainProgram(sw, "Test", "", 1, ""); writer.WriteMainProgram(sw, "Test", "", new List<Plate> { new(48, 96) }, "");
Assert.Contains("G20 G90", sb.ToString()); Assert.Contains("G20 G90", sb.ToString());
} }
@@ -68,7 +70,7 @@ public class CincinnatiPreambleWriterTests
using var sw = new StringWriter(sb); using var sw = new StringWriter(sb);
var writer = new CincinnatiPreambleWriter(config); var writer = new CincinnatiPreambleWriter(config);
writer.WriteMainProgram(sw, "Test", "", 1, ""); writer.WriteMainProgram(sw, "Test", "", new List<Plate> { new(48, 96) }, "");
Assert.Contains("G121 (SMART RAPIDS)", sb.ToString()); Assert.Contains("G121 (SMART RAPIDS)", sb.ToString());
} }
@@ -81,7 +83,7 @@ public class CincinnatiPreambleWriterTests
using var sw = new StringWriter(sb); using var sw = new StringWriter(sb);
var writer = new CincinnatiPreambleWriter(config); var writer = new CincinnatiPreambleWriter(config);
writer.WriteMainProgram(sw, "Test", "", 1, ""); writer.WriteMainProgram(sw, "Test", "", new List<Plate> { new(48, 96) }, "");
Assert.DoesNotContain("G121", sb.ToString()); Assert.DoesNotContain("G121", sb.ToString());
} }
@@ -94,7 +96,7 @@ public class CincinnatiPreambleWriterTests
using var sw = new StringWriter(sb); using var sw = new StringWriter(sb);
var writer = new CincinnatiPreambleWriter(config); var writer = new CincinnatiPreambleWriter(config);
writer.WriteMainProgram(sw, "Test", "", 1, ""); writer.WriteMainProgram(sw, "Test", "", new List<Plate> { new(48, 96) }, "");
Assert.Contains("M50", sb.ToString()); Assert.Contains("M50", sb.ToString());
} }
@@ -107,7 +109,7 @@ public class CincinnatiPreambleWriterTests
using var sw = new StringWriter(sb); using var sw = new StringWriter(sb);
var writer = new CincinnatiPreambleWriter(config); var writer = new CincinnatiPreambleWriter(config);
writer.WriteMainProgram(sw, "Test", "", 1, ""); writer.WriteMainProgram(sw, "Test", "", new List<Plate> { new(48, 96) }, "");
Assert.DoesNotContain("M50", sb.ToString()); Assert.DoesNotContain("M50", sb.ToString());
} }
@@ -120,7 +122,7 @@ public class CincinnatiPreambleWriterTests
using var sw = new StringWriter(sb); using var sw = new StringWriter(sb);
var writer = new CincinnatiPreambleWriter(config); var writer = new CincinnatiPreambleWriter(config);
writer.WriteMainProgram(sw, "Test", "", 1, ""); writer.WriteMainProgram(sw, "Test", "", new List<Plate> { new(48, 96) }, "");
Assert.Contains("G61", sb.ToString()); Assert.Contains("G61", sb.ToString());
} }
@@ -133,11 +135,33 @@ public class CincinnatiPreambleWriterTests
using var sw = new StringWriter(sb); using var sw = new StringWriter(sb);
var writer = new CincinnatiPreambleWriter(config); var writer = new CincinnatiPreambleWriter(config);
writer.WriteMainProgram(sw, "Test", "", 1, ""); writer.WriteMainProgram(sw, "Test", "", new List<Plate> { new(48, 96) }, "");
Assert.DoesNotContain("G61", sb.ToString()); Assert.DoesNotContain("G61", sb.ToString());
} }
[Fact]
public void WriteMainProgram_EmitsLCount_WhenQuantityGreaterThanOne()
{
var config = new CincinnatiPostConfig { PostedUnits = Units.Inches };
var sb = new StringBuilder();
using var sw = new StringWriter(sb);
var writer = new CincinnatiPreambleWriter(config);
var plates = new List<Plate>
{
new(48, 96) { Quantity = 5 },
new(72, 48) { Quantity = 2 },
new(36, 48) { Quantity = 1 }
};
writer.WriteMainProgram(sw, "Test", "", plates, "");
var output = sb.ToString();
Assert.Contains("N1 M98 P101 L5 (LAYOUT 1 - 5 SHEETS)", output);
Assert.Contains("N2 M98 P102 L2 (LAYOUT 2 - 2 SHEETS)", output);
Assert.Contains("N3 M98 P103 (LAYOUT 3)", output);
}
[Fact] [Fact]
public void WriteVariableDeclaration_EmitsSubprogram() public void WriteVariableDeclaration_EmitsSubprogram()
{ {

View File

@@ -28,7 +28,7 @@ public class CincinnatiSheetWriterTests
var output = sb.ToString(); var output = sb.ToString();
Assert.Contains(":101", output); Assert.Contains(":101", output);
Assert.Contains("( Sheet 1 )", output); Assert.Contains("( Layout 1 )", output);
Assert.Contains("#110=", output); Assert.Contains("#110=", output);
Assert.Contains("#111=", output); Assert.Contains("#111=", output);
Assert.Contains("G92 X#5021 Y#5022", output); Assert.Contains("G92 X#5021 Y#5022", output);
@@ -142,7 +142,7 @@ public class CincinnatiSheetWriterTests
} }
[Fact] [Fact]
public void WriteSheet_StartAndEnd_NoM50OnNonLastSheet() public void WriteSheet_StartAndEnd_EmitsM50()
{ {
var config = new CincinnatiPostConfig var config = new CincinnatiPostConfig
{ {
@@ -156,33 +156,33 @@ public class CincinnatiSheetWriterTests
using var sw = new StringWriter(sb); using var sw = new StringWriter(sb);
var sheetWriter = new CincinnatiSheetWriter(config, new ProgramVariableManager()); var sheetWriter = new CincinnatiSheetWriter(config, new ProgramVariableManager());
sheetWriter.Write(sw, plate, "TestNest", 1, 101, "", "", isLastSheet: false); sheetWriter.Write(sw, plate, "TestNest", 1, 101, "", "");
var output = sb.ToString();
Assert.DoesNotContain("M50", output);
}
[Fact]
public void WriteSheet_StartAndEnd_M50OnLastSheet()
{
var config = new CincinnatiPostConfig
{
PalletExchange = PalletMode.StartAndEnd,
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, "", "", isLastSheet: true);
var output = sb.ToString(); var output = sb.ToString();
Assert.Contains("M50", output); Assert.Contains("M50", output);
} }
[Fact]
public void WriteSheet_NoPalletExchange_OmitsM50()
{
var config = new CincinnatiPostConfig
{
PalletExchange = PalletMode.None,
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.DoesNotContain("M50", output);
}
[Fact] [Fact]
public void WriteSheet_EndOfSheet_AlwaysEmitsM50() public void WriteSheet_EndOfSheet_AlwaysEmitsM50()
{ {
@@ -198,7 +198,7 @@ public class CincinnatiSheetWriterTests
using var sw = new StringWriter(sb); using var sw = new StringWriter(sb);
var sheetWriter = new CincinnatiSheetWriter(config, new ProgramVariableManager()); var sheetWriter = new CincinnatiSheetWriter(config, new ProgramVariableManager());
sheetWriter.Write(sw, plate, "TestNest", 1, 101, "", "", isLastSheet: false); sheetWriter.Write(sw, plate, "TestNest", 1, 101, "", "");
var output = sb.ToString(); var output = sb.ToString();
Assert.Contains("M50", output); Assert.Contains("M50", output);