From a2f7219db384d988da62af10cba5453d515be1bc Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Mon, 23 Mar 2026 05:46:46 -0400 Subject: [PATCH] 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) --- .../CincinnatiFeatureWriter.cs | 62 +++++---- .../CincinnatiPartSubprogramWriter.cs | 44 ++++-- .../CincinnatiPreambleWriter.cs | 10 +- .../CincinnatiSheetWriter.cs | 131 +++++++++++------- .../CincinnatiFeatureWriterTests.cs | 75 ++++++++-- .../CincinnatiPostProcessorTests.cs | 43 ++++-- .../CincinnatiPreambleWriterTests.cs | 15 +- .../Cincinnati/CincinnatiSheetWriterTests.cs | 89 +++++++++++- 8 files changed, 334 insertions(+), 135 deletions(-) diff --git a/OpenNest.Posts.Cincinnati/CincinnatiFeatureWriter.cs b/OpenNest.Posts.Cincinnati/CincinnatiFeatureWriter.cs index 6435afe..1602059 100644 --- a/OpenNest.Posts.Cincinnati/CincinnatiFeatureWriter.cs +++ b/OpenNest.Posts.Cincinnati/CincinnatiFeatureWriter.cs @@ -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; } diff --git a/OpenNest.Posts.Cincinnati/CincinnatiPartSubprogramWriter.cs b/OpenNest.Posts.Cincinnati/CincinnatiPartSubprogramWriter.cs index 7a01984..de46883 100644 --- a/OpenNest.Posts.Cincinnati/CincinnatiPartSubprogramWriter.cs +++ b/OpenNest.Posts.Cincinnati/CincinnatiPartSubprogramWriter.cs @@ -27,19 +27,22 @@ public sealed class CincinnatiPartSubprogramWriter /// The program coordinates must already be normalized to origin (0,0). /// 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 codes, bool isEtch)> OrderFeatures(List> features) + { + var result = new List<(List, bool)>(); + var etch = new List>(); + var cut = new List>(); + + 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; } /// diff --git a/OpenNest.Posts.Cincinnati/CincinnatiPreambleWriter.cs b/OpenNest.Posts.Cincinnati/CincinnatiPreambleWriter.cs index 0e32ccd..f119b42 100644 --- a/OpenNest.Posts.Cincinnati/CincinnatiPreambleWriter.cs +++ b/OpenNest.Posts.Cincinnati/CincinnatiPreambleWriter.cs @@ -21,7 +21,9 @@ public sealed class CincinnatiPreambleWriter /// /// Writes the main program header block. /// - public void WriteMainProgram(TextWriter w, string nestName, string materialDescription, int sheetCount) + /// Resolved G89 library file for the initial process setup. + 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"); diff --git a/OpenNest.Posts.Cincinnati/CincinnatiSheetWriter.cs b/OpenNest.Posts.Cincinnati/CincinnatiSheetWriter.cs index 38ba8f3..42220e8 100644 --- a/OpenNest.Posts.Cincinnati/CincinnatiSheetWriter.cs +++ b/OpenNest.Posts.Cincinnati/CincinnatiSheetWriter.cs @@ -30,11 +30,14 @@ public sealed class CincinnatiSheetWriter /// /// Writes a complete sheet subprogram for the given plate. /// + /// Resolved G89 library file for cut operations. + /// Resolved G89 library file for etch operations. /// /// Optional mapping of (drawingId, rotationKey) to sub-program number. /// When provided, non-cutoff parts are emitted as M98 calls instead of inline features. /// 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 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 allParts, - string libraryFile, double sheetDiagonal) + string cutLibrary, string etchLibrary, double sheetDiagonal) { - // Multi-contour splitting - var features = new List<(Part part, List codes)>(); + // Split and classify features, ordering etch before cut per part + var features = new List<(Part part, List codes, bool isEtch)>(); foreach (var part in allParts) { - List current = null; - foreach (var code in part.Program.Codes) - { - if (code is RapidMove) - { - if (current != null) - features.Add((part, current)); - current = new List { code }; - } - else - { - current ??= new List(); - 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> SplitPartFeatures(Part part) + /// + /// Splits a part's program into features (by rapids), classifies each as etch or cut, + /// and orders etch features before cut features. + /// + public static List<(List codes, bool isEtch)> SplitAndOrderFeatures(Part part) { - var features = new List>(); + var etchFeatures = new List>(); + var cutFeatures = new List>(); List 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 { 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, 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 codes, + List> etchFeatures, List> cutFeatures) + { + if (IsFeatureEtch(codes)) + etchFeatures.Add(codes); + else + cutFeatures.Add(codes); + } + + /// + /// A feature is etch if any non-rapid move has LayerType.Scribe. + /// + public static bool IsFeatureEtch(List 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 codes) diff --git a/OpenNest.Tests/Cincinnati/CincinnatiFeatureWriterTests.cs b/OpenNest.Tests/Cincinnati/CincinnatiFeatureWriterTests.cs index 450544c..bf88a41 100644 --- a/OpenNest.Tests/Cincinnati/CincinnatiFeatureWriterTests.cs +++ b/OpenNest.Tests/Cincinnati/CincinnatiFeatureWriterTests.cs @@ -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 + { + 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")); diff --git a/OpenNest.Tests/Cincinnati/CincinnatiPostProcessorTests.cs b/OpenNest.Tests/Cincinnati/CincinnatiPostProcessorTests.cs index 3ed710c..d99c237 100644 --- a/OpenNest.Tests/Cincinnati/CincinnatiPostProcessorTests.cs +++ b/OpenNest.Tests/Cincinnati/CincinnatiPostProcessorTests.cs @@ -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(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 diff --git a/OpenNest.Tests/Cincinnati/CincinnatiPreambleWriterTests.cs b/OpenNest.Tests/Cincinnati/CincinnatiPreambleWriterTests.cs index 3486b2b..b663880 100644 --- a/OpenNest.Tests/Cincinnati/CincinnatiPreambleWriterTests.cs +++ b/OpenNest.Tests/Cincinnati/CincinnatiPreambleWriterTests.cs @@ -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()); } diff --git a/OpenNest.Tests/Cincinnati/CincinnatiSheetWriterTests.cs b/OpenNest.Tests/Cincinnati/CincinnatiSheetWriterTests.cs index a74b718..24841f7 100644 --- a/OpenNest.Tests/Cincinnati/CincinnatiSheetWriterTests.cs +++ b/OpenNest.Tests/Cincinnati/CincinnatiSheetWriterTests.cs @@ -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 + { + 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 + { + 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 + { + new RapidMove(0, 0) + }; + + Assert.False(CincinnatiSheetWriter.IsFeatureEtch(codes)); + } + private static Program CreateSimpleProgram() { var pgm = new Program();