fix: add proper spacing between G-code words in Cincinnati post output

G-code output was concatenated without spaces (e.g. N1005G0X1.4375Y-0.6562).
Now emits standard spacing (N1005 G0 X1.4375 Y-0.6562) across all motion
commands, line numbers, kerf comp, feedrates, M-codes, and comments.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-23 05:46:46 -04:00
parent 7e4040ba08
commit a2f7219db3
8 changed files with 334 additions and 135 deletions

View File

@@ -19,6 +19,7 @@ public sealed class FeatureContext
public bool IsLastFeatureOnSheet { get; set; }
public bool IsSafetyHeadraise { get; set; }
public bool IsExteriorFeature { get; set; }
public bool IsEtch { get; set; }
public string LibraryFile { get; set; } = "";
public double CutDistance { get; set; }
public double SheetDiagonal { get; set; }
@@ -61,17 +62,24 @@ public sealed class CincinnatiFeatureWriter
if (ctx.IsFirstFeatureOfPart && !string.IsNullOrEmpty(ctx.PartName))
writer.WriteLine(CoordinateFormatter.Comment($"PART: {ctx.PartName}"));
// 3. G89 process params (if RepeatG89BeforeEachFeature)
if (_config.RepeatG89BeforeEachFeature && _config.ProcessParameterMode == G89Mode.LibraryFile)
// 3. G89 process params
if (_config.ProcessParameterMode == G89Mode.LibraryFile)
{
var lib = !string.IsNullOrEmpty(ctx.LibraryFile) ? ctx.LibraryFile : _config.DefaultLibraryFile;
var speedClass = _speedClassifier.Classify(ctx.CutDistance, ctx.SheetDiagonal);
var cutDist = _speedClassifier.FormatCutDist(ctx.CutDistance, ctx.SheetDiagonal);
writer.WriteLine($"G89 P {lib} ({speedClass} {cutDist})");
var lib = ctx.LibraryFile;
if (!string.IsNullOrEmpty(lib))
{
var speedClass = _speedClassifier.Classify(ctx.CutDistance, ctx.SheetDiagonal);
var cutDist = _speedClassifier.FormatCutDist(ctx.CutDistance, ctx.SheetDiagonal);
writer.WriteLine($"G89 P {lib} ({speedClass} {cutDist})");
}
else
{
writer.WriteLine("(WARNING: No library found)");
}
}
// 4. Pierce and start cut
writer.WriteLine("G84");
// 4. Pierce/beam on — G85 for etch (no pierce), G84 for cut
writer.WriteLine(ctx.IsEtch ? "G85" : "G84");
// 5. Anti-dive off
if (_config.UseAntiDive)
@@ -90,20 +98,20 @@ public sealed class CincinnatiFeatureWriter
{
var sb = new StringBuilder();
// Kerf compensation on first cutting move
if (!kerfEmitted && _config.KerfCompensation == KerfMode.ControllerSide)
// Kerf compensation on first cutting move (skip for etch)
if (!ctx.IsEtch && !kerfEmitted && _config.KerfCompensation == KerfMode.ControllerSide)
{
sb.Append(_config.DefaultKerfSide == KerfSide.Left ? "G41" : "G42");
sb.Append(_config.DefaultKerfSide == KerfSide.Left ? "G41 " : "G42 ");
kerfEmitted = true;
}
sb.Append($"G1X{_fmt.FormatCoord(linear.EndPoint.X)}Y{_fmt.FormatCoord(linear.EndPoint.Y)}");
sb.Append($"G1 X{_fmt.FormatCoord(linear.EndPoint.X)} Y{_fmt.FormatCoord(linear.EndPoint.Y)}");
// Feedrate
var feedVar = GetFeedVariable(linear.Layer);
// Feedrate — etch always uses process feedrate
var feedVar = ctx.IsEtch ? "#148" : GetFeedVariable(linear.Layer);
if (feedVar != lastFeedVar)
{
sb.Append($"F{feedVar}");
sb.Append($" F{feedVar}");
lastFeedVar = feedVar;
}
@@ -114,28 +122,30 @@ public sealed class CincinnatiFeatureWriter
{
var sb = new StringBuilder();
// Kerf compensation on first cutting move
if (!kerfEmitted && _config.KerfCompensation == KerfMode.ControllerSide)
// Kerf compensation on first cutting move (skip for etch)
if (!ctx.IsEtch && !kerfEmitted && _config.KerfCompensation == KerfMode.ControllerSide)
{
sb.Append(_config.DefaultKerfSide == KerfSide.Left ? "G41" : "G42");
sb.Append(_config.DefaultKerfSide == KerfSide.Left ? "G41 " : "G42 ");
kerfEmitted = true;
}
// G2 = CW, G3 = CCW
var gCode = arc.Rotation == RotationType.CW ? "G2" : "G3";
sb.Append($"{gCode}X{_fmt.FormatCoord(arc.EndPoint.X)}Y{_fmt.FormatCoord(arc.EndPoint.Y)}");
sb.Append($"{gCode} X{_fmt.FormatCoord(arc.EndPoint.X)} Y{_fmt.FormatCoord(arc.EndPoint.Y)}");
// Convert absolute center to incremental I/J
var i = arc.CenterPoint.X - currentPos.X;
var j = arc.CenterPoint.Y - currentPos.Y;
sb.Append($"I{_fmt.FormatCoord(i)}J{_fmt.FormatCoord(j)}");
sb.Append($" I{_fmt.FormatCoord(i)} J{_fmt.FormatCoord(j)}");
// Feedrate — full circles use multiplied feedrate
// Feedrate — etch always uses process feedrate, cut uses layer-based
var isFullCircle = IsFullCircle(currentPos, arc.EndPoint);
var feedVar = isFullCircle ? "[#148*#128]" : GetFeedVariable(arc.Layer);
var feedVar = ctx.IsEtch ? "#148"
: isFullCircle ? "[#148*#128]"
: GetFeedVariable(arc.Layer);
if (feedVar != lastFeedVar)
{
sb.Append($"F{feedVar}");
sb.Append($" F{feedVar}");
lastFeedVar = feedVar;
}
@@ -183,9 +193,9 @@ public sealed class CincinnatiFeatureWriter
var sb = new StringBuilder();
if (_config.UseLineNumbers)
sb.Append($"N{featureNumber}");
sb.Append($"N{featureNumber} ");
sb.Append($"G0X{_fmt.FormatCoord(piercePoint.X)}Y{_fmt.FormatCoord(piercePoint.Y)}");
sb.Append($"G0 X{_fmt.FormatCoord(piercePoint.X)} Y{_fmt.FormatCoord(piercePoint.Y)}");
writer.WriteLine(sb.ToString());
}
@@ -194,7 +204,7 @@ public sealed class CincinnatiFeatureWriter
{
if (ctx.IsSafetyHeadraise && _config.SafetyHeadraiseDistance.HasValue)
{
writer.WriteLine($"M47 P{_config.SafetyHeadraiseDistance.Value}(Safety Headraise)");
writer.WriteLine($"M47 P{_config.SafetyHeadraiseDistance.Value} (Safety Headraise)");
return;
}

View File

@@ -27,19 +27,22 @@ public sealed class CincinnatiPartSubprogramWriter
/// The program coordinates must already be normalized to origin (0,0).
/// </summary>
public void Write(TextWriter w, Program normalizedProgram, string drawingName,
int subNumber, string libraryFile, double sheetDiagonal)
int subNumber, string cutLibrary, string etchLibrary, double sheetDiagonal)
{
var features = SplitFeatures(normalizedProgram.Codes);
if (features.Count == 0)
var allFeatures = SplitFeatures(normalizedProgram.Codes);
if (allFeatures.Count == 0)
return;
// Classify and order: etch features first, then cut features
var ordered = OrderFeatures(allFeatures);
w.WriteLine("(*****************************************************)");
w.WriteLine($":{subNumber}");
w.WriteLine(CoordinateFormatter.Comment($"PART: {drawingName}"));
for (var i = 0; i < features.Count; i++)
for (var i = 0; i < ordered.Count; i++)
{
var codes = features[i];
var (codes, isEtch) = ordered[i];
var featureNumber = i == 0
? _config.FeatureLineNumberStart
: 1000 + i + 1;
@@ -51,10 +54,11 @@ public sealed class CincinnatiPartSubprogramWriter
FeatureNumber = featureNumber,
PartName = drawingName,
IsFirstFeatureOfPart = false,
IsLastFeatureOnSheet = i == features.Count - 1,
IsLastFeatureOnSheet = i == ordered.Count - 1,
IsSafetyHeadraise = false,
IsExteriorFeature = false,
LibraryFile = libraryFile,
IsEtch = isEtch,
LibraryFile = isEtch ? etchLibrary : cutLibrary,
CutDistance = cutDistance,
SheetDiagonal = sheetDiagonal
};
@@ -62,8 +66,30 @@ public sealed class CincinnatiPartSubprogramWriter
_featureWriter.Write(w, ctx);
}
w.WriteLine("G0X0Y0");
w.WriteLine($"M99(END OF {drawingName})");
w.WriteLine("G0 X0 Y0");
w.WriteLine($"M99 (END OF {drawingName})");
}
internal static List<(List<ICode> codes, bool isEtch)> OrderFeatures(List<List<ICode>> features)
{
var result = new List<(List<ICode>, bool)>();
var etch = new List<List<ICode>>();
var cut = new List<List<ICode>>();
foreach (var f in features)
{
if (CincinnatiSheetWriter.IsFeatureEtch(f))
etch.Add(f);
else
cut.Add(f);
}
foreach (var f in etch)
result.Add((f, true));
foreach (var f in cut)
result.Add((f, false));
return result;
}
/// <summary>

View File

@@ -21,7 +21,9 @@ public sealed class CincinnatiPreambleWriter
/// <summary>
/// Writes the main program header block.
/// </summary>
public void WriteMainProgram(TextWriter w, string nestName, string materialDescription, int sheetCount)
/// <param name="initialLibrary">Resolved G89 library file for the initial process setup.</param>
public void WriteMainProgram(TextWriter w, string nestName, string materialDescription,
int sheetCount, string initialLibrary)
{
w.WriteLine(CoordinateFormatter.Comment($"NEST {nestName}"));
w.WriteLine(CoordinateFormatter.Comment($"CONFIGURATION - {_config.ConfigurationName}"));
@@ -39,8 +41,8 @@ public sealed class CincinnatiPreambleWriter
w.WriteLine("M42");
if (_config.ProcessParameterMode == G89Mode.LibraryFile && !string.IsNullOrEmpty(_config.DefaultLibraryFile))
w.WriteLine($"G89 P {_config.DefaultLibraryFile}");
if (_config.ProcessParameterMode == G89Mode.LibraryFile && !string.IsNullOrEmpty(initialLibrary))
w.WriteLine($"G89 P {initialLibrary}");
w.WriteLine($"M98 P{_config.VariableDeclarationSubprogram} (Variable Declaration)");
@@ -49,7 +51,7 @@ public sealed class CincinnatiPreambleWriter
for (var i = 1; i <= sheetCount; i++)
{
var subNum = _config.SheetSubprogramStart + (i - 1);
w.WriteLine($"N{i}M98 P{subNum} (SHEET {i})");
w.WriteLine($"N{i} M98 P{subNum} (SHEET {i})");
}
w.WriteLine("M42");

View File

@@ -30,11 +30,14 @@ public sealed class CincinnatiSheetWriter
/// <summary>
/// Writes a complete sheet subprogram for the given plate.
/// </summary>
/// <param name="cutLibrary">Resolved G89 library file for cut operations.</param>
/// <param name="etchLibrary">Resolved G89 library file for etch operations.</param>
/// <param name="partSubprograms">
/// Optional mapping of (drawingId, rotationKey) to sub-program number.
/// When provided, non-cutoff parts are emitted as M98 calls instead of inline features.
/// </param>
public void Write(TextWriter w, Plate plate, string nestName, int sheetIndex, int subNumber,
string cutLibrary, string etchLibrary,
Dictionary<(int, long), int> partSubprograms = null)
{
if (plate.Parts.Count == 0)
@@ -43,7 +46,6 @@ public sealed class CincinnatiSheetWriter
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);
@@ -55,20 +57,20 @@ public sealed class CincinnatiSheetWriter
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)");
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("G92 X#5021 Y#5022");
if (!string.IsNullOrEmpty(cutLibrary))
w.WriteLine($"G89 P {cutLibrary}");
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("M47");
if (!string.IsNullOrEmpty(cutLibrary))
w.WriteLine($"G89 P {cutLibrary}");
w.WriteLine("GOTO1( Goto Feature )");
// 3. Order parts: non-cutoff sorted by Bottom then Left, cutoffs last
@@ -86,20 +88,20 @@ public sealed class CincinnatiSheetWriter
// 4. Emit parts
if (partSubprograms != null)
WritePartsWithSubprograms(w, allParts, libraryFile, sheetDiagonal, partSubprograms);
WritePartsWithSubprograms(w, allParts, cutLibrary, etchLibrary, sheetDiagonal, partSubprograms);
else
WritePartsInline(w, allParts, libraryFile, sheetDiagonal);
WritePartsInline(w, allParts, cutLibrary, etchLibrary, sheetDiagonal);
// 5. Footer
w.WriteLine("M42");
w.WriteLine("G0X0Y0");
w.WriteLine("G0 X0 Y0");
if (_config.PalletExchange != PalletMode.None)
w.WriteLine($"N{sheetIndex + 1}M50");
w.WriteLine($"M99(END OF {nestName}.{sheetIndex:D3})");
w.WriteLine($"N{sheetIndex + 1} M50");
w.WriteLine($"M99 (END OF {nestName}.{sheetIndex:D3})");
}
private void WritePartsWithSubprograms(TextWriter w, List<Part> allParts,
string libraryFile, double sheetDiagonal,
string cutLibrary, string etchLibrary, double sheetDiagonal,
Dictionary<(int, long), int> partSubprograms)
{
var lastPartName = "";
@@ -126,26 +128,28 @@ public sealed class CincinnatiSheetWriter
else
{
// Inline features for cutoffs or parts without sub-programs
var features = SplitPartFeatures(part);
var features = SplitAndOrderFeatures(part);
for (var f = 0; f < features.Count; f++)
{
var (codes, isEtch) = features[f];
var featureNumber = featureIndex == 0
? _config.FeatureLineNumberStart
: 1000 + featureIndex + 1;
var isLastFeature = isLastPart && f == features.Count - 1;
var cutDistance = ComputeCutDistance(features[f]);
var cutDistance = ComputeCutDistance(codes);
var ctx = new FeatureContext
{
Codes = features[f],
Codes = codes,
FeatureNumber = featureNumber,
PartName = partName,
IsFirstFeatureOfPart = isNewPart && f == 0,
IsLastFeatureOnSheet = isLastFeature,
IsSafetyHeadraise = isSafetyHeadraise && f == 0,
IsExteriorFeature = false,
LibraryFile = libraryFile,
IsEtch = isEtch,
LibraryFile = isEtch ? etchLibrary : cutLibrary,
CutDistance = cutDistance,
SheetDiagonal = sheetDiagonal
};
@@ -164,7 +168,7 @@ public sealed class CincinnatiSheetWriter
{
// Safety headraise before rapid to new part
if (isSafetyHeadraise && _config.SafetyHeadraiseDistance.HasValue)
w.WriteLine($"M47 P{_config.SafetyHeadraiseDistance.Value}(Safety Headraise)");
w.WriteLine($"M47 P{_config.SafetyHeadraiseDistance.Value} (Safety Headraise)");
// Rapid to part position (bounding box lower-left)
var featureNumber = featureIndex == 0
@@ -173,21 +177,21 @@ public sealed class CincinnatiSheetWriter
var sb = new StringBuilder();
if (_config.UseLineNumbers)
sb.Append($"N{featureNumber}");
sb.Append($"G0X{_fmt.FormatCoord(part.Left)}Y{_fmt.FormatCoord(part.Bottom)}");
sb.Append($"N{featureNumber} ");
sb.Append($"G0 X{_fmt.FormatCoord(part.Left)} Y{_fmt.FormatCoord(part.Bottom)}");
w.WriteLine(sb.ToString());
// Part name comment
w.WriteLine(CoordinateFormatter.Comment($"PART: {partName}"));
// Set local coordinate system at part position
w.WriteLine("G92X0Y0");
w.WriteLine("G92 X0 Y0");
// Call part sub-program
w.WriteLine($"M98P{subNum}({partName})");
w.WriteLine($"M98 P{subNum} ({partName})");
// Restore sheet coordinate system
w.WriteLine($"G92X{_fmt.FormatCoord(part.Left)}Y{_fmt.FormatCoord(part.Bottom)}");
w.WriteLine($"G92 X{_fmt.FormatCoord(part.Left)} Y{_fmt.FormatCoord(part.Bottom)}");
// Head raise (unless last part on sheet)
if (!isLastPart)
@@ -195,36 +199,22 @@ public sealed class CincinnatiSheetWriter
}
private void WritePartsInline(TextWriter w, List<Part> allParts,
string libraryFile, double sheetDiagonal)
string cutLibrary, string etchLibrary, double sheetDiagonal)
{
// Multi-contour splitting
var features = new List<(Part part, List<ICode> codes)>();
// Split and classify features, ordering etch before cut per part
var features = new List<(Part part, List<ICode> codes, bool isEtch)>();
foreach (var part in allParts)
{
List<ICode> current = null;
foreach (var code in part.Program.Codes)
{
if (code is RapidMove)
{
if (current != null)
features.Add((part, current));
current = new List<ICode> { code };
}
else
{
current ??= new List<ICode>();
current.Add(code);
}
}
if (current != null && current.Count > 0)
features.Add((part, current));
var partFeatures = SplitAndOrderFeatures(part);
foreach (var (codes, isEtch) in partFeatures)
features.Add((part, codes, isEtch));
}
// Emit features
var lastPartName = "";
for (var i = 0; i < features.Count; i++)
{
var (part, codes) = features[i];
var (part, codes, isEtch) = features[i];
var partName = part.BaseDrawing.Name;
var isFirstFeatureOfPart = partName != lastPartName;
var isSafetyHeadraise = partName != lastPartName && lastPartName != "";
@@ -245,7 +235,8 @@ public sealed class CincinnatiSheetWriter
IsLastFeatureOnSheet = isLastFeature,
IsSafetyHeadraise = isSafetyHeadraise,
IsExteriorFeature = false,
LibraryFile = libraryFile,
IsEtch = isEtch,
LibraryFile = isEtch ? etchLibrary : cutLibrary,
CutDistance = cutDistance,
SheetDiagonal = sheetDiagonal
};
@@ -255,9 +246,14 @@ public sealed class CincinnatiSheetWriter
}
}
private static List<List<ICode>> SplitPartFeatures(Part part)
/// <summary>
/// Splits a part's program into features (by rapids), classifies each as etch or cut,
/// and orders etch features before cut features.
/// </summary>
public static List<(List<ICode> codes, bool isEtch)> SplitAndOrderFeatures(Part part)
{
var features = new List<List<ICode>>();
var etchFeatures = new List<List<ICode>>();
var cutFeatures = new List<List<ICode>>();
List<ICode> current = null;
foreach (var code in part.Program.Codes)
@@ -265,7 +261,7 @@ public sealed class CincinnatiSheetWriter
if (code is RapidMove)
{
if (current != null)
features.Add(current);
ClassifyAndAdd(current, etchFeatures, cutFeatures);
current = new List<ICode> { code };
}
else
@@ -276,9 +272,40 @@ public sealed class CincinnatiSheetWriter
}
if (current != null && current.Count > 0)
features.Add(current);
ClassifyAndAdd(current, etchFeatures, cutFeatures);
return features;
// Etch features first, then cut features
var result = new List<(List<ICode>, bool)>();
foreach (var f in etchFeatures)
result.Add((f, true));
foreach (var f in cutFeatures)
result.Add((f, false));
return result;
}
private static void ClassifyAndAdd(List<ICode> codes,
List<List<ICode>> etchFeatures, List<List<ICode>> cutFeatures)
{
if (IsFeatureEtch(codes))
etchFeatures.Add(codes);
else
cutFeatures.Add(codes);
}
/// <summary>
/// A feature is etch if any non-rapid move has LayerType.Scribe.
/// </summary>
public static bool IsFeatureEtch(List<ICode> codes)
{
foreach (var code in codes)
{
if (code is LinearMove linear && linear.Layer == LayerType.Scribe)
return true;
if (code is ArcMove arc && arc.Layer == LayerType.Scribe)
return true;
}
return false;
}
private static double ComputeCutDistance(List<ICode> codes)

View File

@@ -13,9 +13,7 @@ public class CincinnatiFeatureWriterTests
UseAntiDive = true,
KerfCompensation = KerfMode.ControllerSide,
DefaultKerfSide = KerfSide.Left,
RepeatG89BeforeEachFeature = true,
ProcessParameterMode = G89Mode.LibraryFile,
DefaultLibraryFile = "MILD10",
InteriorM47 = M47Mode.Always,
ExteriorM47 = M47Mode.Always,
UseSpeedGas = false,
@@ -58,7 +56,7 @@ public class CincinnatiFeatureWriterTests
var output = WriteFeature(config, ctx);
var lines = output.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries);
Assert.StartsWith("N1G0X13.401Y57.4895", lines[0]);
Assert.StartsWith("N1 G0 X13.401 Y57.4895", lines[0]);
}
[Fact]
@@ -70,7 +68,7 @@ public class CincinnatiFeatureWriterTests
var output = WriteFeature(config, ctx);
var lines = output.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries);
Assert.StartsWith("G0X13.401Y57.4895", lines[0]);
Assert.StartsWith("G0 X13.401 Y57.4895", lines[0]);
}
[Fact]
@@ -260,8 +258,8 @@ public class CincinnatiFeatureWriterTests
var cwOutput = WriteFeature(config, SimpleContext(cwCodes));
var ccwOutput = WriteFeature(config, SimpleContext(ccwCodes));
Assert.Contains("G2X", cwOutput);
Assert.Contains("G3X", ccwOutput);
Assert.Contains("G2 X", cwOutput);
Assert.Contains("G3 X", ccwOutput);
}
[Fact]
@@ -289,12 +287,10 @@ public class CincinnatiFeatureWriterTests
}
[Fact]
public void G89_EmittedWhenRepeatEnabled()
public void G89_EmittedWithLibraryFile()
{
var config = DefaultConfig();
config.RepeatG89BeforeEachFeature = true;
config.ProcessParameterMode = G89Mode.LibraryFile;
config.DefaultLibraryFile = "MILD10";
var ctx = SimpleContext();
ctx.LibraryFile = "MILD10";
ctx.CutDistance = 18.0;
@@ -305,14 +301,65 @@ public class CincinnatiFeatureWriterTests
}
[Fact]
public void G89_NotEmittedWhenRepeatDisabled()
public void G89_WarningEmittedWhenNoLibrary()
{
var config = DefaultConfig();
config.RepeatG89BeforeEachFeature = false;
config.ProcessParameterMode = G89Mode.LibraryFile;
var ctx = SimpleContext();
ctx.LibraryFile = "";
var output = WriteFeature(config, ctx);
Assert.DoesNotContain("G89", output);
Assert.Contains("WARNING: No library found", output);
Assert.DoesNotContain("G89 P", output);
}
[Fact]
public void Etch_UsesG85InsteadOfG84()
{
var config = DefaultConfig();
var ctx = SimpleContext();
ctx.IsEtch = true;
ctx.LibraryFile = "EtchN2.lib";
var output = WriteFeature(config, ctx);
Assert.Contains("G85", output);
Assert.DoesNotContain("G84", output);
}
[Fact]
public void Etch_SkipsKerfCompensation()
{
var config = DefaultConfig();
config.KerfCompensation = KerfMode.ControllerSide;
var ctx = SimpleContext();
ctx.IsEtch = true;
ctx.LibraryFile = "EtchN2.lib";
var output = WriteFeature(config, ctx);
Assert.DoesNotContain("G41", output);
Assert.DoesNotContain("G42", output);
Assert.DoesNotContain("G40", output);
}
[Fact]
public void Etch_AllMovesUseProcessFeedrate()
{
var config = DefaultConfig();
config.KerfCompensation = KerfMode.PreApplied;
var codes = new List<ICode>
{
new RapidMove(1.0, 1.0),
new LinearMove(2.0, 1.0) { Layer = LayerType.Leadin },
new LinearMove(3.0, 1.0) { Layer = LayerType.Cut }
};
var ctx = SimpleContext(codes);
ctx.IsEtch = true;
ctx.LibraryFile = "EtchN2.lib";
var output = WriteFeature(config, ctx);
// Should use #148 for all moves, not #126 for lead-in
Assert.DoesNotContain("F#126", output);
Assert.Contains("F#148", output);
}
[Fact]
@@ -378,7 +425,7 @@ public class CincinnatiFeatureWriterTests
ctx.IsLastFeatureOnSheet = false;
var output = WriteFeature(config, ctx);
Assert.Contains("M47 P2000(Safety Headraise)", output);
Assert.Contains("M47 P2000 (Safety Headraise)", output);
}
[Fact]
@@ -404,7 +451,7 @@ public class CincinnatiFeatureWriterTests
var lines = output.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries);
// Find indices of key lines
var rapidIdx = Array.FindIndex(lines, l => l.Contains("G0X"));
var rapidIdx = Array.FindIndex(lines, l => l.Contains("G0 X"));
var partIdx = Array.FindIndex(lines, l => l.Contains("PART:"));
var g89Idx = Array.FindIndex(lines, l => l.Contains("G89"));
var g84Idx = Array.FindIndex(lines, l => l.Contains("G84"));

View File

@@ -17,7 +17,6 @@ public class CincinnatiPostProcessorTests
var config = new CincinnatiPostConfig
{
ConfigurationName = "CL940",
DefaultLibraryFile = "MS135N2PANEL.lib",
PostedAccuracy = 4
};
var post = new CincinnatiPostProcessor(config);
@@ -72,7 +71,7 @@ public class CincinnatiPostProcessorTests
var output = Encoding.UTF8.GetString(ms.ToArray());
// Should only have one sheet subprogram call in main
Assert.Contains("N1M98 P101 (SHEET 1)", output);
Assert.Contains("N1 M98 P101 (SHEET 1)", output);
Assert.DoesNotContain("SHEET 2", output);
}
@@ -104,10 +103,19 @@ public class CincinnatiPostProcessorTests
var config = new CincinnatiPostConfig
{
ConfigurationName = "CL940_CORONA",
DefaultLibraryFile = "MS135N2PANEL.lib",
DefaultAssistGas = "N2",
DefaultEtchGas = "N2",
PostedUnits = Units.Inches,
KerfCompensation = KerfMode.ControllerSide,
UseAntiDive = true
UseAntiDive = true,
MaterialLibraries = new()
{
new MaterialLibraryEntry { Material = "Mild Steel", Thickness = 0.135, Gas = "N2", Library = "MS135N2PANEL.lib" }
},
EtchLibraries = new()
{
new EtchLibraryEntry { Gas = "N2", Library = "EtchN2.lib" }
}
};
var opts = new JsonSerializerOptions
@@ -119,10 +127,15 @@ public class CincinnatiPostProcessorTests
var deserialized = JsonSerializer.Deserialize<CincinnatiPostConfig>(json, opts);
Assert.Equal("CL940_CORONA", deserialized.ConfigurationName);
Assert.Equal("MS135N2PANEL.lib", deserialized.DefaultLibraryFile);
Assert.Equal("N2", deserialized.DefaultAssistGas);
Assert.Equal("N2", deserialized.DefaultEtchGas);
Assert.Equal(Units.Inches, deserialized.PostedUnits);
Assert.Equal(KerfMode.ControllerSide, deserialized.KerfCompensation);
Assert.True(deserialized.UseAntiDive);
Assert.Single(deserialized.MaterialLibraries);
Assert.Equal("MS135N2PANEL.lib", deserialized.MaterialLibraries[0].Library);
Assert.Single(deserialized.EtchLibraries);
Assert.Equal("EtchN2.lib", deserialized.EtchLibraries[0].Library);
// Enums serialize as strings
Assert.Contains("\"Inches\"", json);
@@ -157,21 +170,21 @@ public class CincinnatiPostProcessorTests
var output = Encoding.UTF8.GetString(ms.ToArray());
// Sheet should contain M98 call to part sub-program
Assert.Contains("M98P200", output);
Assert.Contains("M98 P200", output);
// Should have G92 for local coordinate positioning
Assert.Contains("G92X0Y0", output);
Assert.Contains("G92 X0 Y0", output);
// Part sub-program definition
Assert.Contains(":200", output);
Assert.Contains("G84", output);
// Sub-program ends with G0X0Y0 and M99
Assert.Contains("G0X0Y0", output);
Assert.Contains("M99(END OF Square)", output);
// Sub-program ends with G0 X0 Y0 and M99
Assert.Contains("G0 X0 Y0", output);
Assert.Contains("M99 (END OF Square)", output);
// G92 restore after M98 call
Assert.Contains("G92X", output);
Assert.Contains("G92 X", output);
}
[Fact]
@@ -198,7 +211,7 @@ public class CincinnatiPostProcessorTests
var output = Encoding.UTF8.GetString(ms.ToArray());
// Both parts should call the same sub-program
var m98Count = System.Text.RegularExpressions.Regex.Matches(output, "M98P200").Count;
var m98Count = System.Text.RegularExpressions.Regex.Matches(output, @"M98 P200\b").Count;
Assert.Equal(2, m98Count);
// Only one sub-program definition
@@ -238,8 +251,8 @@ public class CincinnatiPostProcessorTests
// Should have two different sub-programs
Assert.Contains(":200", output);
Assert.Contains(":201", output);
Assert.Contains("M98P200", output);
Assert.Contains("M98P201", output);
Assert.Contains("M98 P200", output);
Assert.Contains("M98 P201", output);
}
[Fact]
@@ -268,7 +281,7 @@ public class CincinnatiPostProcessorTests
var output = Encoding.UTF8.GetString(ms.ToArray());
// Regular part uses sub-program
Assert.Contains("M98P200", output);
Assert.Contains("M98 P200", output);
Assert.Contains(":200", output);
// Cutoff should NOT have its own sub-program

View File

@@ -13,14 +13,13 @@ public class CincinnatiPreambleWriterTests
var config = new CincinnatiPostConfig
{
ConfigurationName = "CL940",
PostedUnits = Units.Inches,
DefaultLibraryFile = "MS135N2PANEL.lib"
PostedUnits = Units.Inches
};
var sb = new StringBuilder();
using var sw = new StringWriter(sb);
var writer = new CincinnatiPreambleWriter(config);
writer.WriteMainProgram(sw, "TestNest", "Mild Steel, 10GA", 2);
writer.WriteMainProgram(sw, "TestNest", "Mild Steel, 10GA", 2, "MS135N2PANEL.lib");
var output = sb.ToString();
Assert.Contains("( NEST TestNest )", output);
@@ -30,8 +29,8 @@ public class CincinnatiPreambleWriterTests
Assert.Contains("G89 P MS135N2PANEL.lib", output);
Assert.Contains("M98 P100 (Variable Declaration)", output);
Assert.Contains("GOTO1 (GOTO SHEET NUMBER)", output);
Assert.Contains("N1M98 P101 (SHEET 1)", output);
Assert.Contains("N2M98 P102 (SHEET 2)", output);
Assert.Contains("N1 M98 P101 (SHEET 1)", output);
Assert.Contains("N2 M98 P102 (SHEET 2)", output);
Assert.Contains("M30 (END OF MAIN)", output);
}
@@ -43,7 +42,7 @@ public class CincinnatiPreambleWriterTests
using var sw = new StringWriter(sb);
var writer = new CincinnatiPreambleWriter(config);
writer.WriteMainProgram(sw, "Test", "", 1);
writer.WriteMainProgram(sw, "Test", "", 1, "");
Assert.Contains("G21", sb.ToString());
}
@@ -56,7 +55,7 @@ public class CincinnatiPreambleWriterTests
using var sw = new StringWriter(sb);
var writer = new CincinnatiPreambleWriter(config);
writer.WriteMainProgram(sw, "Test", "", 1);
writer.WriteMainProgram(sw, "Test", "", 1, "");
Assert.Contains("G61", sb.ToString());
}
@@ -69,7 +68,7 @@ public class CincinnatiPreambleWriterTests
using var sw = new StringWriter(sb);
var writer = new CincinnatiPreambleWriter(config);
writer.WriteMainProgram(sw, "Test", "", 1);
writer.WriteMainProgram(sw, "Test", "", 1, "");
Assert.DoesNotContain("G61", sb.ToString());
}

View File

@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
@@ -14,7 +15,6 @@ public class CincinnatiSheetWriterTests
{
var config = new CincinnatiPostConfig
{
DefaultLibraryFile = "MS135N2PANEL.lib",
PostedAccuracy = 4
};
var plate = new Plate(48.0, 96.0);
@@ -24,14 +24,15 @@ public class CincinnatiSheetWriterTests
using var sw = new StringWriter(sb);
var sheetWriter = new CincinnatiSheetWriter(config, new ProgramVariableManager());
sheetWriter.Write(sw, plate, "TestNest", 1, 101);
sheetWriter.Write(sw, plate, "TestNest", 1, 101, "MS135N2PANEL.lib", "EtchN2.lib");
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("G92 X#5021 Y#5022", output);
Assert.Contains("G89 P MS135N2PANEL.lib", output);
Assert.Contains("M99", output);
}
@@ -50,11 +51,11 @@ public class CincinnatiSheetWriterTests
using var sw = new StringWriter(sb);
var sheetWriter = new CincinnatiSheetWriter(config, new ProgramVariableManager());
sheetWriter.Write(sw, plate, "TestNest", 1, 101);
sheetWriter.Write(sw, plate, "TestNest", 1, 101, "", "");
var output = sb.ToString();
Assert.Contains("M42", output);
Assert.Contains("G0X0Y0", output);
Assert.Contains("G0 X0 Y0", output);
Assert.Contains("M50", output);
}
@@ -68,7 +69,7 @@ public class CincinnatiSheetWriterTests
using var sw = new StringWriter(sb);
var sheetWriter = new CincinnatiSheetWriter(config, new ProgramVariableManager());
sheetWriter.Write(sw, plate, "TestNest", 1, 101);
sheetWriter.Write(sw, plate, "TestNest", 1, 101, "", "");
Assert.Equal("", sb.ToString());
}
@@ -96,7 +97,7 @@ public class CincinnatiSheetWriterTests
using var sw = new StringWriter(sb);
var sheetWriter = new CincinnatiSheetWriter(config, new ProgramVariableManager());
sheetWriter.Write(sw, plate, "TestNest", 1, 101);
sheetWriter.Write(sw, plate, "TestNest", 1, 101, "", "");
var output = sb.ToString();
// Should have two G84 pierce commands (one per contour)
@@ -104,6 +105,80 @@ public class CincinnatiSheetWriterTests
Assert.Equal(2, g84Count);
}
[Fact]
public void WriteSheet_EtchFeaturesOrderedBeforeCut()
{
var config = new CincinnatiPostConfig { PostedAccuracy = 4 };
var pgm = new Program();
// Cut contour first in program
pgm.Codes.Add(new RapidMove(0, 0));
pgm.Codes.Add(new LinearMove(5, 0) { Layer = LayerType.Cut });
pgm.Codes.Add(new LinearMove(5, 5) { Layer = LayerType.Cut });
// Etch contour second in program
pgm.Codes.Add(new RapidMove(1, 1));
pgm.Codes.Add(new LinearMove(2, 1) { Layer = LayerType.Scribe });
pgm.Codes.Add(new LinearMove(2, 2) { Layer = LayerType.Scribe });
var plate = new Plate(48.0, 96.0);
plate.Parts.Add(new Part(new Drawing("MixedPart", 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, "MS250O2.lib", "EtchN2.lib");
var output = sb.ToString();
// Etch (G85) should appear before cut (G84)
var g85Idx = output.IndexOf("G85");
var g84Idx = output.IndexOf("G84");
Assert.True(g85Idx >= 0, "G85 should be present for etch");
Assert.True(g84Idx >= 0, "G84 should be present for cut");
Assert.True(g85Idx < g84Idx, "G85 (etch) should come before G84 (cut)");
// Etch uses etch library
Assert.Contains("G89 P EtchN2.lib", output);
// Cut uses cut library
Assert.Contains("G89 P MS250O2.lib", output);
}
[Fact]
public void IsFeatureEtch_ReturnsTrueForScribeLayer()
{
var codes = new List<ICode>
{
new RapidMove(0, 0),
new LinearMove(1, 0) { Layer = LayerType.Scribe },
new LinearMove(1, 1) { Layer = LayerType.Scribe }
};
Assert.True(CincinnatiSheetWriter.IsFeatureEtch(codes));
}
[Fact]
public void IsFeatureEtch_ReturnsFalseForCutLayer()
{
var codes = new List<ICode>
{
new RapidMove(0, 0),
new LinearMove(1, 0) { Layer = LayerType.Cut },
new LinearMove(1, 1) { Layer = LayerType.Cut }
};
Assert.False(CincinnatiSheetWriter.IsFeatureEtch(codes));
}
[Fact]
public void IsFeatureEtch_ReturnsFalseForRapidsOnly()
{
var codes = new List<ICode>
{
new RapidMove(0, 0)
};
Assert.False(CincinnatiSheetWriter.IsFeatureEtch(codes));
}
private static Program CreateSimpleProgram()
{
var pgm = new Program();