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)