diff --git a/OpenNest.Posts.Cincinnati/CincinnatiPartSubprogramWriter.cs b/OpenNest.Posts.Cincinnati/CincinnatiPartSubprogramWriter.cs index 38f944b..8b8a9b5 100644 --- a/OpenNest.Posts.Cincinnati/CincinnatiPartSubprogramWriter.cs +++ b/OpenNest.Posts.Cincinnati/CincinnatiPartSubprogramWriter.cs @@ -70,6 +70,26 @@ public sealed class CincinnatiPartSubprogramWriter w.WriteLine($"M99 (END OF {drawingName})"); } + /// + /// If the program has no leading rapid, inserts a synthetic rapid at the + /// last motion endpoint (the contour return point). This ensures the feature + /// writer knows the true pierce location and preserves the first contour segment. + /// + internal static void EnsureLeadingRapid(Program pgm) + { + if (pgm.Codes.Count == 0 || pgm.Codes[0] is RapidMove) + return; + + for (var i = pgm.Codes.Count - 1; i >= 0; i--) + { + if (pgm.Codes[i] is Motion lastMotion) + { + pgm.Codes.Insert(0, new RapidMove(lastMotion.EndPoint)); + return; + } + } + } + /// /// Creates a sub-program key for matching parts to their sub-programs. /// @@ -103,6 +123,13 @@ public sealed class CincinnatiPartSubprogramWriter var bbox = pgm.BoundingBox(); pgm.Offset(-bbox.Location.X, -bbox.Location.Y); + // If the program has no leading rapid, the feature writer + // will use the first motion endpoint as the pierce point, + // losing the first contour segment. Insert a synthetic rapid + // at the contour's return point (last motion endpoint) so + // the full contour is preserved. + EnsureLeadingRapid(pgm); + entries.Add((subNum, part.BaseDrawing.Name, pgm)); } } diff --git a/OpenNest.Posts.Cincinnati/FeatureUtils.cs b/OpenNest.Posts.Cincinnati/FeatureUtils.cs index e4384b5..d31d3a2 100644 --- a/OpenNest.Posts.Cincinnati/FeatureUtils.cs +++ b/OpenNest.Posts.Cincinnati/FeatureUtils.cs @@ -72,7 +72,27 @@ public static class FeatureUtils public static List<(List codes, bool isEtch)> SplitAndClassify(Part part) { part.Program.Mode = Mode.Absolute; - return ClassifyAndOrder(SplitByRapids(part.Program.Codes)); + var codes = part.Program.Codes; + + // If no leading rapid, the first contour segment would be lost because + // the feature writer pierces at the first motion endpoint. Insert a + // synthetic rapid at the contour's return point to preserve closure. + if (codes.Count > 0 && codes[0] is not RapidMove) + { + for (var i = codes.Count - 1; i >= 0; i--) + { + if (codes[i] is Motion lastMotion) + { + var withRapid = new List(codes.Count + 1); + withRapid.Add(new RapidMove(lastMotion.EndPoint)); + withRapid.AddRange(codes); + codes = withRapid; + break; + } + } + } + + return ClassifyAndOrder(SplitByRapids(codes)); } /// diff --git a/OpenNest.Tests/Cincinnati/CincinnatiPostProcessorTests.cs b/OpenNest.Tests/Cincinnati/CincinnatiPostProcessorTests.cs index 3d4fdf2..024ff54 100644 --- a/OpenNest.Tests/Cincinnati/CincinnatiPostProcessorTests.cs +++ b/OpenNest.Tests/Cincinnati/CincinnatiPostProcessorTests.cs @@ -388,6 +388,57 @@ public class CincinnatiPostProcessorTests Assert.Equal(300, deserialized.PartSubprogramStart); } + [Fact] + public void Post_ClosedContourWithoutRapid_OutputsAllSegments() + { + // Reproduce bug: a closed contour with no leading rapid loses its + // first segment in the CNC output because the feature writer uses + // the first LinearMove endpoint as the pierce point. + var pgm = new Program(Mode.Incremental); + pgm.Codes.Add(new LinearMove(0, 2)); // (0,0) → (0,2) + pgm.Codes.Add(new LinearMove(2, 0)); // (0,2) → (2,2) + pgm.Codes.Add(new LinearMove(0, -2)); // (2,2) → (2,0) + pgm.Codes.Add(new LinearMove(-2, 0)); // (2,0) → (0,0) + + var drawing = new Drawing("ClosedSquare", pgm); + var nest = new Nest("TestClosure"); + nest.Drawings.Add(drawing); + var plate = new Plate(24, 24); + plate.Parts.Add(new Part(drawing, new Vector(1, 1))); + nest.Plates.Add(plate); + + var config = new CincinnatiPostConfig + { + UsePartSubprograms = true, + PostedAccuracy = 4 + }; + var post = new CincinnatiPostProcessor(config); + + using var ms = new MemoryStream(); + post.Post(nest, ms); + var output = Encoding.UTF8.GetString(ms.ToArray()); + + // The subprogram should contain all 4 segments of the square. + // Without the fix, the first segment (0,0)→(0,2) was lost + // because the feature writer pierced at (0,2) making it zero-length. + var lines = output.Split('\n').Select(l => l.Trim()).ToArray(); + + // Count G1 moves in the subprogram (should be 4 for a square) + var subStart = Array.FindIndex(lines, l => l.StartsWith(":200") || l.StartsWith(":300")); + Assert.True(subStart >= 0, "Expected a part subprogram"); + + var g1Count = 0; + for (var i = subStart; i < lines.Length; i++) + { + if (lines[i].StartsWith("M99")) + break; + if (lines[i].Contains("G1 ")) + g1Count++; + } + + Assert.Equal(4, g1Count); + } + private static Nest CreateTestNest() { var nest = new Nest("TestNest"); diff --git a/OpenNest/Forms/BomImportForm.cs b/OpenNest/Forms/BomImportForm.cs index 23f5003..fde561b 100644 --- a/OpenNest/Forms/BomImportForm.cs +++ b/OpenNest/Forms/BomImportForm.cs @@ -432,7 +432,6 @@ namespace OpenNest.Forms var rapid = (RapidMove)pgm[0]; drawing.Source.Offset = rapid.EndPoint; pgm.Offset(-rapid.EndPoint); - pgm.Codes.RemoveAt(0); } drawing.Program = pgm; diff --git a/OpenNest/Forms/CadConverterForm.cs b/OpenNest/Forms/CadConverterForm.cs index a6103a5..2ad71a3 100644 --- a/OpenNest/Forms/CadConverterForm.cs +++ b/OpenNest/Forms/CadConverterForm.cs @@ -362,7 +362,6 @@ namespace OpenNest.Forms var rapid = (RapidMove)pgm[0]; originOffset = rapid.EndPoint; pgm.Offset(-originOffset); - pgm.Codes.RemoveAt(0); } var drawing = new Drawing(item.Name, pgm); @@ -660,7 +659,8 @@ namespace OpenNest.Forms var rapid = (RapidMove)firstCode; drawing.Source.Offset = rapid.EndPoint; pgm.Offset(-rapid.EndPoint); - pgm.Codes.RemoveAt(0); + // Keep the rapid (now at origin) — it marks the contour + // start and is needed by the post for correct pierce placement. } if (item == CurrentItem && programEditor.IsDirty && programEditor.Program != null)