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
@@ -19,6 +19,7 @@ public sealed class FeatureContext
public bool IsLastFeatureOnSheet { get; set; } public bool IsLastFeatureOnSheet { get; set; }
public bool IsSafetyHeadraise { get; set; } public bool IsSafetyHeadraise { get; set; }
public bool IsExteriorFeature { get; set; } public bool IsExteriorFeature { get; set; }
public bool IsEtch { get; set; }
public string LibraryFile { get; set; } = ""; public string LibraryFile { get; set; } = "";
public double CutDistance { get; set; } public double CutDistance { get; set; }
public double SheetDiagonal { get; set; } public double SheetDiagonal { get; set; }
@@ -61,17 +62,24 @@ public sealed class CincinnatiFeatureWriter
if (ctx.IsFirstFeatureOfPart && !string.IsNullOrEmpty(ctx.PartName)) if (ctx.IsFirstFeatureOfPart && !string.IsNullOrEmpty(ctx.PartName))
writer.WriteLine(CoordinateFormatter.Comment($"PART: {ctx.PartName}")); writer.WriteLine(CoordinateFormatter.Comment($"PART: {ctx.PartName}"));
// 3. G89 process params (if RepeatG89BeforeEachFeature) // 3. G89 process params
if (_config.RepeatG89BeforeEachFeature && _config.ProcessParameterMode == G89Mode.LibraryFile) if (_config.ProcessParameterMode == G89Mode.LibraryFile)
{
var lib = ctx.LibraryFile;
if (!string.IsNullOrEmpty(lib))
{ {
var lib = !string.IsNullOrEmpty(ctx.LibraryFile) ? ctx.LibraryFile : _config.DefaultLibraryFile;
var speedClass = _speedClassifier.Classify(ctx.CutDistance, ctx.SheetDiagonal); var speedClass = _speedClassifier.Classify(ctx.CutDistance, ctx.SheetDiagonal);
var cutDist = _speedClassifier.FormatCutDist(ctx.CutDistance, ctx.SheetDiagonal); var cutDist = _speedClassifier.FormatCutDist(ctx.CutDistance, ctx.SheetDiagonal);
writer.WriteLine($"G89 P {lib} ({speedClass} {cutDist})"); writer.WriteLine($"G89 P {lib} ({speedClass} {cutDist})");
} }
else
{
writer.WriteLine("(WARNING: No library found)");
}
}
// 4. Pierce and start cut // 4. Pierce/beam on — G85 for etch (no pierce), G84 for cut
writer.WriteLine("G84"); writer.WriteLine(ctx.IsEtch ? "G85" : "G84");
// 5. Anti-dive off // 5. Anti-dive off
if (_config.UseAntiDive) if (_config.UseAntiDive)
@@ -90,20 +98,20 @@ public sealed class CincinnatiFeatureWriter
{ {
var sb = new StringBuilder(); var sb = new StringBuilder();
// Kerf compensation on first cutting move // Kerf compensation on first cutting move (skip for etch)
if (!kerfEmitted && _config.KerfCompensation == KerfMode.ControllerSide) 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; 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 // Feedrate — etch always uses process feedrate
var feedVar = GetFeedVariable(linear.Layer); var feedVar = ctx.IsEtch ? "#148" : GetFeedVariable(linear.Layer);
if (feedVar != lastFeedVar) if (feedVar != lastFeedVar)
{ {
sb.Append($"F{feedVar}"); sb.Append($" F{feedVar}");
lastFeedVar = feedVar; lastFeedVar = feedVar;
} }
@@ -114,28 +122,30 @@ public sealed class CincinnatiFeatureWriter
{ {
var sb = new StringBuilder(); var sb = new StringBuilder();
// Kerf compensation on first cutting move // Kerf compensation on first cutting move (skip for etch)
if (!kerfEmitted && _config.KerfCompensation == KerfMode.ControllerSide) 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; kerfEmitted = true;
} }
// G2 = CW, G3 = CCW // G2 = CW, G3 = CCW
var gCode = arc.Rotation == RotationType.CW ? "G2" : "G3"; 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 // Convert absolute center to incremental I/J
var i = arc.CenterPoint.X - currentPos.X; var i = arc.CenterPoint.X - currentPos.X;
var j = arc.CenterPoint.Y - currentPos.Y; 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 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) if (feedVar != lastFeedVar)
{ {
sb.Append($"F{feedVar}"); sb.Append($" F{feedVar}");
lastFeedVar = feedVar; lastFeedVar = feedVar;
} }
@@ -183,9 +193,9 @@ public sealed class CincinnatiFeatureWriter
var sb = new StringBuilder(); var sb = new StringBuilder();
if (_config.UseLineNumbers) 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()); writer.WriteLine(sb.ToString());
} }
@@ -194,7 +204,7 @@ public sealed class CincinnatiFeatureWriter
{ {
if (ctx.IsSafetyHeadraise && _config.SafetyHeadraiseDistance.HasValue) 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; return;
} }
@@ -27,19 +27,22 @@ public sealed class CincinnatiPartSubprogramWriter
/// The program coordinates must already be normalized to origin (0,0). /// The program coordinates must already be normalized to origin (0,0).
/// </summary> /// </summary>
public void Write(TextWriter w, Program normalizedProgram, string drawingName, 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); var allFeatures = SplitFeatures(normalizedProgram.Codes);
if (features.Count == 0) if (allFeatures.Count == 0)
return; return;
// Classify and order: etch features first, then cut features
var ordered = OrderFeatures(allFeatures);
w.WriteLine("(*****************************************************)"); w.WriteLine("(*****************************************************)");
w.WriteLine($":{subNumber}"); w.WriteLine($":{subNumber}");
w.WriteLine(CoordinateFormatter.Comment($"PART: {drawingName}")); 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 var featureNumber = i == 0
? _config.FeatureLineNumberStart ? _config.FeatureLineNumberStart
: 1000 + i + 1; : 1000 + i + 1;
@@ -51,10 +54,11 @@ public sealed class CincinnatiPartSubprogramWriter
FeatureNumber = featureNumber, FeatureNumber = featureNumber,
PartName = drawingName, PartName = drawingName,
IsFirstFeatureOfPart = false, IsFirstFeatureOfPart = false,
IsLastFeatureOnSheet = i == features.Count - 1, IsLastFeatureOnSheet = i == ordered.Count - 1,
IsSafetyHeadraise = false, IsSafetyHeadraise = false,
IsExteriorFeature = false, IsExteriorFeature = false,
LibraryFile = libraryFile, IsEtch = isEtch,
LibraryFile = isEtch ? etchLibrary : cutLibrary,
CutDistance = cutDistance, CutDistance = cutDistance,
SheetDiagonal = sheetDiagonal SheetDiagonal = sheetDiagonal
}; };
@@ -62,8 +66,30 @@ public sealed class CincinnatiPartSubprogramWriter
_featureWriter.Write(w, ctx); _featureWriter.Write(w, ctx);
} }
w.WriteLine("G0X0Y0"); w.WriteLine("G0 X0 Y0");
w.WriteLine($"M99(END OF {drawingName})"); 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> /// <summary>
@@ -21,7 +21,9 @@ public sealed class CincinnatiPreambleWriter
/// <summary> /// <summary>
/// Writes the main program header block. /// Writes the main program header block.
/// </summary> /// </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($"NEST {nestName}"));
w.WriteLine(CoordinateFormatter.Comment($"CONFIGURATION - {_config.ConfigurationName}")); w.WriteLine(CoordinateFormatter.Comment($"CONFIGURATION - {_config.ConfigurationName}"));
@@ -39,8 +41,8 @@ public sealed class CincinnatiPreambleWriter
w.WriteLine("M42"); w.WriteLine("M42");
if (_config.ProcessParameterMode == G89Mode.LibraryFile && !string.IsNullOrEmpty(_config.DefaultLibraryFile)) if (_config.ProcessParameterMode == G89Mode.LibraryFile && !string.IsNullOrEmpty(initialLibrary))
w.WriteLine($"G89 P {_config.DefaultLibraryFile}"); w.WriteLine($"G89 P {initialLibrary}");
w.WriteLine($"M98 P{_config.VariableDeclarationSubprogram} (Variable Declaration)"); w.WriteLine($"M98 P{_config.VariableDeclarationSubprogram} (Variable Declaration)");
@@ -49,7 +51,7 @@ public sealed class CincinnatiPreambleWriter
for (var i = 1; i <= sheetCount; i++) for (var i = 1; i <= sheetCount; i++)
{ {
var subNum = _config.SheetSubprogramStart + (i - 1); 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"); w.WriteLine("M42");
@@ -30,11 +30,14 @@ public sealed class CincinnatiSheetWriter
/// <summary> /// <summary>
/// Writes a complete sheet subprogram for the given plate. /// Writes a complete sheet subprogram for the given plate.
/// </summary> /// </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"> /// <param name="partSubprograms">
/// 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 sheetIndex, int subNumber,
string cutLibrary, string etchLibrary,
Dictionary<(int, long), int> partSubprograms = null) Dictionary<(int, long), int> partSubprograms = null)
{ {
if (plate.Parts.Count == 0) if (plate.Parts.Count == 0)
@@ -43,7 +46,6 @@ public sealed class CincinnatiSheetWriter
var width = plate.Size.Width; var width = plate.Size.Width;
var length = plate.Size.Length; var length = plate.Size.Length;
var sheetDiagonal = System.Math.Sqrt(width * width + length * length); var sheetDiagonal = System.Math.Sqrt(width * width + length * length);
var libraryFile = _config.DefaultLibraryFile ?? "";
var varDeclSub = _config.VariableDeclarationSubprogram; var varDeclSub = _config.VariableDeclarationSubprogram;
var partCount = plate.Parts.Count(p => !p.BaseDrawing.IsCutOff); var partCount = plate.Parts.Count(p => !p.BaseDrawing.IsCutOff);
@@ -55,20 +57,20 @@ public sealed class CincinnatiSheetWriter
w.WriteLine($"( Layout {sheetIndex} )"); w.WriteLine($"( Layout {sheetIndex} )");
w.WriteLine($"( SHEET NAME = {_fmt.FormatCoord(length)} X {_fmt.FormatCoord(width)} )"); 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)");
// 2. Coordinate setup // 2. Coordinate setup
w.WriteLine("M42"); w.WriteLine("M42");
w.WriteLine("N10000"); w.WriteLine("N10000");
w.WriteLine("G92X#5021Y#5022"); w.WriteLine("G92 X#5021 Y#5022");
if (!string.IsNullOrEmpty(libraryFile)) if (!string.IsNullOrEmpty(cutLibrary))
w.WriteLine($"G89 P {libraryFile}"); w.WriteLine($"G89 P {cutLibrary}");
w.WriteLine($"M98 P{varDeclSub} (Variable Declaration)"); w.WriteLine($"M98 P{varDeclSub} (Variable Declaration)");
w.WriteLine("G90"); w.WriteLine("G90");
w.WriteLine("M47(CPT)"); w.WriteLine("M47");
if (!string.IsNullOrEmpty(libraryFile)) if (!string.IsNullOrEmpty(cutLibrary))
w.WriteLine($"G89 P {libraryFile}"); w.WriteLine($"G89 P {cutLibrary}");
w.WriteLine("GOTO1( Goto Feature )"); w.WriteLine("GOTO1( Goto Feature )");
// 3. Order parts: non-cutoff sorted by Bottom then Left, cutoffs last // 3. Order parts: non-cutoff sorted by Bottom then Left, cutoffs last
@@ -86,20 +88,20 @@ public sealed class CincinnatiSheetWriter
// 4. Emit parts // 4. Emit parts
if (partSubprograms != null) if (partSubprograms != null)
WritePartsWithSubprograms(w, allParts, libraryFile, sheetDiagonal, partSubprograms); WritePartsWithSubprograms(w, allParts, cutLibrary, etchLibrary, sheetDiagonal, partSubprograms);
else else
WritePartsInline(w, allParts, libraryFile, sheetDiagonal); WritePartsInline(w, allParts, cutLibrary, etchLibrary, sheetDiagonal);
// 5. Footer // 5. Footer
w.WriteLine("M42"); w.WriteLine("M42");
w.WriteLine("G0X0Y0"); w.WriteLine("G0 X0 Y0");
if (_config.PalletExchange != PalletMode.None) if (_config.PalletExchange != PalletMode.None)
w.WriteLine($"N{sheetIndex + 1}M50"); w.WriteLine($"N{sheetIndex + 1} M50");
w.WriteLine($"M99(END OF {nestName}.{sheetIndex:D3})"); w.WriteLine($"M99 (END OF {nestName}.{sheetIndex:D3})");
} }
private void WritePartsWithSubprograms(TextWriter w, List<Part> allParts, private void WritePartsWithSubprograms(TextWriter w, List<Part> allParts,
string libraryFile, double sheetDiagonal, string cutLibrary, string etchLibrary, double sheetDiagonal,
Dictionary<(int, long), int> partSubprograms) Dictionary<(int, long), int> partSubprograms)
{ {
var lastPartName = ""; var lastPartName = "";
@@ -126,26 +128,28 @@ public sealed class CincinnatiSheetWriter
else else
{ {
// Inline features for cutoffs or parts without sub-programs // 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++) for (var f = 0; f < features.Count; f++)
{ {
var (codes, isEtch) = features[f];
var featureNumber = featureIndex == 0 var featureNumber = featureIndex == 0
? _config.FeatureLineNumberStart ? _config.FeatureLineNumberStart
: 1000 + featureIndex + 1; : 1000 + featureIndex + 1;
var isLastFeature = isLastPart && f == features.Count - 1; var isLastFeature = isLastPart && f == features.Count - 1;
var cutDistance = ComputeCutDistance(features[f]); var cutDistance = ComputeCutDistance(codes);
var ctx = new FeatureContext var ctx = new FeatureContext
{ {
Codes = features[f], Codes = codes,
FeatureNumber = featureNumber, FeatureNumber = featureNumber,
PartName = partName, PartName = partName,
IsFirstFeatureOfPart = isNewPart && f == 0, IsFirstFeatureOfPart = isNewPart && f == 0,
IsLastFeatureOnSheet = isLastFeature, IsLastFeatureOnSheet = isLastFeature,
IsSafetyHeadraise = isSafetyHeadraise && f == 0, IsSafetyHeadraise = isSafetyHeadraise && f == 0,
IsExteriorFeature = false, IsExteriorFeature = false,
LibraryFile = libraryFile, IsEtch = isEtch,
LibraryFile = isEtch ? etchLibrary : cutLibrary,
CutDistance = cutDistance, CutDistance = cutDistance,
SheetDiagonal = sheetDiagonal SheetDiagonal = sheetDiagonal
}; };
@@ -164,7 +168,7 @@ public sealed class CincinnatiSheetWriter
{ {
// Safety headraise before rapid to new part // Safety headraise before rapid to new part
if (isSafetyHeadraise && _config.SafetyHeadraiseDistance.HasValue) 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) // Rapid to part position (bounding box lower-left)
var featureNumber = featureIndex == 0 var featureNumber = featureIndex == 0
@@ -173,21 +177,21 @@ public sealed class CincinnatiSheetWriter
var sb = new StringBuilder(); var sb = new StringBuilder();
if (_config.UseLineNumbers) if (_config.UseLineNumbers)
sb.Append($"N{featureNumber}"); sb.Append($"N{featureNumber} ");
sb.Append($"G0X{_fmt.FormatCoord(part.Left)}Y{_fmt.FormatCoord(part.Bottom)}"); sb.Append($"G0 X{_fmt.FormatCoord(part.Left)} Y{_fmt.FormatCoord(part.Bottom)}");
w.WriteLine(sb.ToString()); w.WriteLine(sb.ToString());
// Part name comment // Part name comment
w.WriteLine(CoordinateFormatter.Comment($"PART: {partName}")); w.WriteLine(CoordinateFormatter.Comment($"PART: {partName}"));
// Set local coordinate system at part position // Set local coordinate system at part position
w.WriteLine("G92X0Y0"); w.WriteLine("G92 X0 Y0");
// Call part sub-program // Call part sub-program
w.WriteLine($"M98P{subNum}({partName})"); w.WriteLine($"M98 P{subNum} ({partName})");
// Restore sheet coordinate system // 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) // Head raise (unless last part on sheet)
if (!isLastPart) if (!isLastPart)
@@ -195,36 +199,22 @@ public sealed class CincinnatiSheetWriter
} }
private void WritePartsInline(TextWriter w, List<Part> allParts, private void WritePartsInline(TextWriter w, List<Part> allParts,
string libraryFile, double sheetDiagonal) string cutLibrary, string etchLibrary, double sheetDiagonal)
{ {
// Multi-contour splitting // Split and classify features, ordering etch before cut per part
var features = new List<(Part part, List<ICode> codes)>(); var features = new List<(Part part, List<ICode> codes, bool isEtch)>();
foreach (var part in allParts) foreach (var part in allParts)
{ {
List<ICode> current = null; var partFeatures = SplitAndOrderFeatures(part);
foreach (var code in part.Program.Codes) foreach (var (codes, isEtch) in partFeatures)
{ features.Add((part, codes, isEtch));
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));
} }
// Emit features // Emit features
var lastPartName = ""; var lastPartName = "";
for (var i = 0; i < features.Count; i++) 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 partName = part.BaseDrawing.Name;
var isFirstFeatureOfPart = partName != lastPartName; var isFirstFeatureOfPart = partName != lastPartName;
var isSafetyHeadraise = partName != lastPartName && lastPartName != ""; var isSafetyHeadraise = partName != lastPartName && lastPartName != "";
@@ -245,7 +235,8 @@ public sealed class CincinnatiSheetWriter
IsLastFeatureOnSheet = isLastFeature, IsLastFeatureOnSheet = isLastFeature,
IsSafetyHeadraise = isSafetyHeadraise, IsSafetyHeadraise = isSafetyHeadraise,
IsExteriorFeature = false, IsExteriorFeature = false,
LibraryFile = libraryFile, IsEtch = isEtch,
LibraryFile = isEtch ? etchLibrary : cutLibrary,
CutDistance = cutDistance, CutDistance = cutDistance,
SheetDiagonal = sheetDiagonal 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; List<ICode> current = null;
foreach (var code in part.Program.Codes) foreach (var code in part.Program.Codes)
@@ -265,7 +261,7 @@ public sealed class CincinnatiSheetWriter
if (code is RapidMove) if (code is RapidMove)
{ {
if (current != null) if (current != null)
features.Add(current); ClassifyAndAdd(current, etchFeatures, cutFeatures);
current = new List<ICode> { code }; current = new List<ICode> { code };
} }
else else
@@ -276,9 +272,40 @@ public sealed class CincinnatiSheetWriter
} }
if (current != null && current.Count > 0) 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) private static double ComputeCutDistance(List<ICode> codes)
@@ -13,9 +13,7 @@ public class CincinnatiFeatureWriterTests
UseAntiDive = true, UseAntiDive = true,
KerfCompensation = KerfMode.ControllerSide, KerfCompensation = KerfMode.ControllerSide,
DefaultKerfSide = KerfSide.Left, DefaultKerfSide = KerfSide.Left,
RepeatG89BeforeEachFeature = true,
ProcessParameterMode = G89Mode.LibraryFile, ProcessParameterMode = G89Mode.LibraryFile,
DefaultLibraryFile = "MILD10",
InteriorM47 = M47Mode.Always, InteriorM47 = M47Mode.Always,
ExteriorM47 = M47Mode.Always, ExteriorM47 = M47Mode.Always,
UseSpeedGas = false, UseSpeedGas = false,
@@ -58,7 +56,7 @@ public class CincinnatiFeatureWriterTests
var output = WriteFeature(config, ctx); var output = WriteFeature(config, ctx);
var lines = output.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries); 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] [Fact]
@@ -70,7 +68,7 @@ public class CincinnatiFeatureWriterTests
var output = WriteFeature(config, ctx); var output = WriteFeature(config, ctx);
var lines = output.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries); 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] [Fact]
@@ -260,8 +258,8 @@ public class CincinnatiFeatureWriterTests
var cwOutput = WriteFeature(config, SimpleContext(cwCodes)); var cwOutput = WriteFeature(config, SimpleContext(cwCodes));
var ccwOutput = WriteFeature(config, SimpleContext(ccwCodes)); var ccwOutput = WriteFeature(config, SimpleContext(ccwCodes));
Assert.Contains("G2X", cwOutput); Assert.Contains("G2 X", cwOutput);
Assert.Contains("G3X", ccwOutput); Assert.Contains("G3 X", ccwOutput);
} }
[Fact] [Fact]
@@ -289,12 +287,10 @@ public class CincinnatiFeatureWriterTests
} }
[Fact] [Fact]
public void G89_EmittedWhenRepeatEnabled() public void G89_EmittedWithLibraryFile()
{ {
var config = DefaultConfig(); var config = DefaultConfig();
config.RepeatG89BeforeEachFeature = true;
config.ProcessParameterMode = G89Mode.LibraryFile; config.ProcessParameterMode = G89Mode.LibraryFile;
config.DefaultLibraryFile = "MILD10";
var ctx = SimpleContext(); var ctx = SimpleContext();
ctx.LibraryFile = "MILD10"; ctx.LibraryFile = "MILD10";
ctx.CutDistance = 18.0; ctx.CutDistance = 18.0;
@@ -305,14 +301,65 @@ public class CincinnatiFeatureWriterTests
} }
[Fact] [Fact]
public void G89_NotEmittedWhenRepeatDisabled() public void G89_WarningEmittedWhenNoLibrary()
{ {
var config = DefaultConfig(); var config = DefaultConfig();
config.RepeatG89BeforeEachFeature = false; config.ProcessParameterMode = G89Mode.LibraryFile;
var ctx = SimpleContext(); var ctx = SimpleContext();
ctx.LibraryFile = "";
var output = WriteFeature(config, ctx); 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] [Fact]
@@ -378,7 +425,7 @@ public class CincinnatiFeatureWriterTests
ctx.IsLastFeatureOnSheet = false; ctx.IsLastFeatureOnSheet = false;
var output = WriteFeature(config, ctx); var output = WriteFeature(config, ctx);
Assert.Contains("M47 P2000(Safety Headraise)", output); Assert.Contains("M47 P2000 (Safety Headraise)", output);
} }
[Fact] [Fact]
@@ -404,7 +451,7 @@ public class CincinnatiFeatureWriterTests
var lines = output.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries); var lines = output.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries);
// Find indices of key lines // 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 partIdx = Array.FindIndex(lines, l => l.Contains("PART:"));
var g89Idx = Array.FindIndex(lines, l => l.Contains("G89")); var g89Idx = Array.FindIndex(lines, l => l.Contains("G89"));
var g84Idx = Array.FindIndex(lines, l => l.Contains("G84")); var g84Idx = Array.FindIndex(lines, l => l.Contains("G84"));
@@ -17,7 +17,6 @@ public class CincinnatiPostProcessorTests
var config = new CincinnatiPostConfig var config = new CincinnatiPostConfig
{ {
ConfigurationName = "CL940", ConfigurationName = "CL940",
DefaultLibraryFile = "MS135N2PANEL.lib",
PostedAccuracy = 4 PostedAccuracy = 4
}; };
var post = new CincinnatiPostProcessor(config); var post = new CincinnatiPostProcessor(config);
@@ -72,7 +71,7 @@ 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("N1M98 P101 (SHEET 1)", output); Assert.Contains("N1 M98 P101 (SHEET 1)", output);
Assert.DoesNotContain("SHEET 2", output); Assert.DoesNotContain("SHEET 2", output);
} }
@@ -104,10 +103,19 @@ public class CincinnatiPostProcessorTests
var config = new CincinnatiPostConfig var config = new CincinnatiPostConfig
{ {
ConfigurationName = "CL940_CORONA", ConfigurationName = "CL940_CORONA",
DefaultLibraryFile = "MS135N2PANEL.lib", DefaultAssistGas = "N2",
DefaultEtchGas = "N2",
PostedUnits = Units.Inches, PostedUnits = Units.Inches,
KerfCompensation = KerfMode.ControllerSide, 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 var opts = new JsonSerializerOptions
@@ -119,10 +127,15 @@ public class CincinnatiPostProcessorTests
var deserialized = JsonSerializer.Deserialize<CincinnatiPostConfig>(json, opts); var deserialized = JsonSerializer.Deserialize<CincinnatiPostConfig>(json, opts);
Assert.Equal("CL940_CORONA", deserialized.ConfigurationName); 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(Units.Inches, deserialized.PostedUnits);
Assert.Equal(KerfMode.ControllerSide, deserialized.KerfCompensation); Assert.Equal(KerfMode.ControllerSide, deserialized.KerfCompensation);
Assert.True(deserialized.UseAntiDive); 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 // Enums serialize as strings
Assert.Contains("\"Inches\"", json); Assert.Contains("\"Inches\"", json);
@@ -157,21 +170,21 @@ public class CincinnatiPostProcessorTests
var output = Encoding.UTF8.GetString(ms.ToArray()); var output = Encoding.UTF8.GetString(ms.ToArray());
// Sheet should contain M98 call to part sub-program // 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 // Should have G92 for local coordinate positioning
Assert.Contains("G92X0Y0", output); Assert.Contains("G92 X0 Y0", output);
// Part sub-program definition // Part sub-program definition
Assert.Contains(":200", output); Assert.Contains(":200", output);
Assert.Contains("G84", output); Assert.Contains("G84", output);
// Sub-program ends with G0X0Y0 and M99 // Sub-program ends with G0 X0 Y0 and M99
Assert.Contains("G0X0Y0", output); Assert.Contains("G0 X0 Y0", output);
Assert.Contains("M99(END OF Square)", output); Assert.Contains("M99 (END OF Square)", output);
// G92 restore after M98 call // G92 restore after M98 call
Assert.Contains("G92X", output); Assert.Contains("G92 X", output);
} }
[Fact] [Fact]
@@ -198,7 +211,7 @@ public class CincinnatiPostProcessorTests
var output = Encoding.UTF8.GetString(ms.ToArray()); var output = Encoding.UTF8.GetString(ms.ToArray());
// Both parts should call the same sub-program // 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); Assert.Equal(2, m98Count);
// Only one sub-program definition // Only one sub-program definition
@@ -238,8 +251,8 @@ public class CincinnatiPostProcessorTests
// Should have two different sub-programs // Should have two different sub-programs
Assert.Contains(":200", output); Assert.Contains(":200", output);
Assert.Contains(":201", output); Assert.Contains(":201", output);
Assert.Contains("M98P200", output); Assert.Contains("M98 P200", output);
Assert.Contains("M98P201", output); Assert.Contains("M98 P201", output);
} }
[Fact] [Fact]
@@ -268,7 +281,7 @@ public class CincinnatiPostProcessorTests
var output = Encoding.UTF8.GetString(ms.ToArray()); var output = Encoding.UTF8.GetString(ms.ToArray());
// Regular part uses sub-program // Regular part uses sub-program
Assert.Contains("M98P200", output); Assert.Contains("M98 P200", output);
Assert.Contains(":200", output); Assert.Contains(":200", output);
// Cutoff should NOT have its own sub-program // Cutoff should NOT have its own sub-program
@@ -13,14 +13,13 @@ public class CincinnatiPreambleWriterTests
var config = new CincinnatiPostConfig var config = new CincinnatiPostConfig
{ {
ConfigurationName = "CL940", ConfigurationName = "CL940",
PostedUnits = Units.Inches, PostedUnits = Units.Inches
DefaultLibraryFile = "MS135N2PANEL.lib"
}; };
var sb = new StringBuilder(); var sb = new StringBuilder();
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); writer.WriteMainProgram(sw, "TestNest", "Mild Steel, 10GA", 2, "MS135N2PANEL.lib");
var output = sb.ToString(); var output = sb.ToString();
Assert.Contains("( NEST TestNest )", output); Assert.Contains("( NEST TestNest )", output);
@@ -30,8 +29,8 @@ public class CincinnatiPreambleWriterTests
Assert.Contains("G89 P MS135N2PANEL.lib", output); Assert.Contains("G89 P MS135N2PANEL.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("N1M98 P101 (SHEET 1)", output); Assert.Contains("N1 M98 P101 (SHEET 1)", output);
Assert.Contains("N2M98 P102 (SHEET 2)", output); Assert.Contains("N2 M98 P102 (SHEET 2)", output);
Assert.Contains("M30 (END OF MAIN)", output); Assert.Contains("M30 (END OF MAIN)", output);
} }
@@ -43,7 +42,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", "", 1, "");
Assert.Contains("G21", sb.ToString()); Assert.Contains("G21", sb.ToString());
} }
@@ -56,7 +55,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", "", 1, "");
Assert.Contains("G61", sb.ToString()); Assert.Contains("G61", sb.ToString());
} }
@@ -69,7 +68,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", "", 1, "");
Assert.DoesNotContain("G61", sb.ToString()); Assert.DoesNotContain("G61", sb.ToString());
} }
@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
@@ -14,7 +15,6 @@ public class CincinnatiSheetWriterTests
{ {
var config = new CincinnatiPostConfig var config = new CincinnatiPostConfig
{ {
DefaultLibraryFile = "MS135N2PANEL.lib",
PostedAccuracy = 4 PostedAccuracy = 4
}; };
var plate = new Plate(48.0, 96.0); var plate = new Plate(48.0, 96.0);
@@ -24,14 +24,15 @@ 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); sheetWriter.Write(sw, plate, "TestNest", 1, 101, "MS135N2PANEL.lib", "EtchN2.lib");
var output = sb.ToString(); var output = sb.ToString();
Assert.Contains(":101", output); Assert.Contains(":101", output);
Assert.Contains("( Sheet 1 )", output); Assert.Contains("( Sheet 1 )", output);
Assert.Contains("#110=", output); Assert.Contains("#110=", output);
Assert.Contains("#111=", 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); Assert.Contains("M99", output);
} }
@@ -50,11 +51,11 @@ 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); sheetWriter.Write(sw, plate, "TestNest", 1, 101, "", "");
var output = sb.ToString(); var output = sb.ToString();
Assert.Contains("M42", output); Assert.Contains("M42", output);
Assert.Contains("G0X0Y0", output); Assert.Contains("G0 X0 Y0", output);
Assert.Contains("M50", output); Assert.Contains("M50", output);
} }
@@ -68,7 +69,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); sheetWriter.Write(sw, plate, "TestNest", 1, 101, "", "");
Assert.Equal("", sb.ToString()); Assert.Equal("", sb.ToString());
} }
@@ -96,7 +97,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); sheetWriter.Write(sw, plate, "TestNest", 1, 101, "", "");
var output = sb.ToString(); var output = sb.ToString();
// Should have two G84 pierce commands (one per contour) // Should have two G84 pierce commands (one per contour)
@@ -104,6 +105,80 @@ public class CincinnatiSheetWriterTests
Assert.Equal(2, g84Count); 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() private static Program CreateSimpleProgram()
{ {
var pgm = new Program(); var pgm = new Program();