From 568539d5b10c475f98b5ae1eb57fbf84a7056e35 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Tue, 31 Mar 2026 09:50:43 -0400 Subject: [PATCH] fix: offset inline feature coordinates by part location for G90 absolute mode Part.Program stores coordinates relative to the part's own origin, but the Cincinnati post processor emits G90 (absolute positioning). Inline features were writing part-relative coordinates directly without adding Part.Location, producing incorrect output. Sub-program mode was unaffected because it uses G92 to set up local coordinate systems. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../CincinnatiFeatureWriter.cs | 17 ++++-- .../CincinnatiSheetWriter.cs | 6 +- .../Cincinnati/CincinnatiSheetWriterTests.cs | 61 +++++++++++++++++++ 3 files changed, 77 insertions(+), 7 deletions(-) diff --git a/OpenNest.Posts.Cincinnati/CincinnatiFeatureWriter.cs b/OpenNest.Posts.Cincinnati/CincinnatiFeatureWriter.cs index ca5bbde..f2885cb 100644 --- a/OpenNest.Posts.Cincinnati/CincinnatiFeatureWriter.cs +++ b/OpenNest.Posts.Cincinnati/CincinnatiFeatureWriter.cs @@ -23,6 +23,12 @@ public sealed class FeatureContext public string LibraryFile { get; set; } = ""; public double CutDistance { get; set; } public double SheetDiagonal { get; set; } + + /// + /// Part location on the plate. Added to all output X/Y coordinates + /// so part-relative programs become plate-absolute under G90. + /// + public Vector PartLocation { get; set; } = Vector.Zero; } /// @@ -51,12 +57,13 @@ public sealed class CincinnatiFeatureWriter var currentPos = Vector.Zero; var lastFeedVar = ""; var kerfEmitted = false; + var offset = ctx.PartLocation; // Find the pierce point from the first rapid move var piercePoint = FindPiercePoint(ctx.Codes); // 1. Rapid to pierce point (with line number if configured) - WriteRapidToPierce(writer, ctx.FeatureNumber, piercePoint); + WriteRapidToPierce(writer, ctx.FeatureNumber, piercePoint, offset); // 2. Part name comment on first feature of each part if (ctx.IsFirstFeatureOfPart && !string.IsNullOrEmpty(ctx.PartName)) @@ -105,7 +112,7 @@ public sealed class CincinnatiFeatureWriter kerfEmitted = true; } - sb.Append($"G1 X{_fmt.FormatCoord(linear.EndPoint.X)} Y{_fmt.FormatCoord(linear.EndPoint.Y)}"); + sb.Append($"G1 X{_fmt.FormatCoord(linear.EndPoint.X + offset.X)} Y{_fmt.FormatCoord(linear.EndPoint.Y + offset.Y)}"); // Feedrate — etch always uses process feedrate var feedVar = ctx.IsEtch ? "#148" : GetLinearFeedVariable(linear.Layer); @@ -131,7 +138,7 @@ public sealed class CincinnatiFeatureWriter // 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 + offset.X)} Y{_fmt.FormatCoord(arc.EndPoint.Y + offset.Y)}"); // Convert absolute center to incremental I/J var i = arc.CenterPoint.X - currentPos.X; @@ -188,14 +195,14 @@ public sealed class CincinnatiFeatureWriter return Vector.Zero; } - private void WriteRapidToPierce(TextWriter writer, int featureNumber, Vector piercePoint) + private void WriteRapidToPierce(TextWriter writer, int featureNumber, Vector piercePoint, Vector offset) { var sb = new StringBuilder(); if (_config.UseLineNumbers) sb.Append($"N{featureNumber} "); - sb.Append($"G0 X{_fmt.FormatCoord(piercePoint.X)} Y{_fmt.FormatCoord(piercePoint.Y)}"); + sb.Append($"G0 X{_fmt.FormatCoord(piercePoint.X + offset.X)} Y{_fmt.FormatCoord(piercePoint.Y + offset.Y)}"); writer.WriteLine(sb.ToString()); } diff --git a/OpenNest.Posts.Cincinnati/CincinnatiSheetWriter.cs b/OpenNest.Posts.Cincinnati/CincinnatiSheetWriter.cs index 084f876..6a18ef2 100644 --- a/OpenNest.Posts.Cincinnati/CincinnatiSheetWriter.cs +++ b/OpenNest.Posts.Cincinnati/CincinnatiSheetWriter.cs @@ -153,7 +153,8 @@ public sealed class CincinnatiSheetWriter IsEtch = isEtch, LibraryFile = isEtch ? etchLibrary : cutLibrary, CutDistance = cutDistance, - SheetDiagonal = sheetDiagonal + SheetDiagonal = sheetDiagonal, + PartLocation = part.Location }; _featureWriter.Write(w, ctx); @@ -240,7 +241,8 @@ public sealed class CincinnatiSheetWriter IsEtch = isEtch, LibraryFile = isEtch ? etchLibrary : cutLibrary, CutDistance = cutDistance, - SheetDiagonal = sheetDiagonal + SheetDiagonal = sheetDiagonal, + PartLocation = part.Location }; _featureWriter.Write(w, ctx); diff --git a/OpenNest.Tests/Cincinnati/CincinnatiSheetWriterTests.cs b/OpenNest.Tests/Cincinnati/CincinnatiSheetWriterTests.cs index 3fdc8b0..d13279a 100644 --- a/OpenNest.Tests/Cincinnati/CincinnatiSheetWriterTests.cs +++ b/OpenNest.Tests/Cincinnati/CincinnatiSheetWriterTests.cs @@ -280,6 +280,67 @@ public class CincinnatiSheetWriterTests Assert.False(FeatureUtils.IsEtch(codes)); } + [Fact] + public void WriteSheet_InlineCoordinates_AreAbsoluteOnPlate() + { + var config = new CincinnatiPostConfig { PostedAccuracy = 4 }; + // Part program is at origin: (0,0) to (2,0) to (2,2) to (0,2) to (0,0) + var pgm = new Program(); + pgm.Codes.Add(new RapidMove(0, 0)); + pgm.Codes.Add(new LinearMove(2, 0)); + pgm.Codes.Add(new LinearMove(2, 2)); + pgm.Codes.Add(new LinearMove(0, 2)); + pgm.Codes.Add(new LinearMove(0, 0)); + + var plate = new Plate(48.0, 96.0); + // Place part at (10.5, 5.25) on the plate to produce non-integer coordinates + plate.Parts.Add(new Part(new Drawing("Square", pgm), new Vector(10.5, 5.25))); + + var sb = new StringBuilder(); + using var sw = new StringWriter(sb); + var sheetWriter = new CincinnatiSheetWriter(config, new ProgramVariableManager()); + + sheetWriter.Write(sw, plate, "TestNest", 1, 101, "", ""); + + var output = sb.ToString(); + // Under G90, coordinates must be plate-absolute (part coords + part location) + Assert.Contains("G0 X10.5 Y5.25", output); // rapid to pierce + Assert.Contains("G1 X12.5 Y5.25", output); // (2,0) + (10.5,5.25) + Assert.Contains("G1 X12.5 Y7.25", output); // (2,2) + (10.5,5.25) + Assert.Contains("G1 X10.5 Y7.25", output); // (0,2) + (10.5,5.25) + Assert.Contains("G1 X10.5 Y5.25", output); // (0,0) + (10.5,5.25) + } + + [Fact] + public void WriteSheet_TwoPartsAtDifferentLocations_HaveDistinctAbsoluteCoords() + { + var config = new CincinnatiPostConfig { PostedAccuracy = 4 }; + var pgm = new Program(); + pgm.Codes.Add(new RapidMove(0, 0)); + pgm.Codes.Add(new LinearMove(1, 0)); + pgm.Codes.Add(new LinearMove(1, 1)); + pgm.Codes.Add(new LinearMove(0, 0)); + + var drawing = new Drawing("Tri", pgm); + var plate = new Plate(48.0, 96.0); + plate.Parts.Add(new Part(drawing, new Vector(5.5, 3.25))); + plate.Parts.Add(new Part(drawing, new Vector(20.5, 10.25))); + + var sb = new StringBuilder(); + using var sw = new StringWriter(sb); + var sheetWriter = new CincinnatiSheetWriter(config, new ProgramVariableManager()); + + sheetWriter.Write(sw, plate, "TestNest", 1, 101, "", ""); + + var output = sb.ToString(); + // First part at (5.5, 3.25) + Assert.Contains("G0 X5.5 Y3.25", output); + Assert.Contains("G1 X6.5 Y3.25", output); + // Second part at (20.5, 10.25) + Assert.Contains("G0 X20.5 Y10.25", output); + Assert.Contains("G1 X21.5 Y10.25", output); + } + private static Program CreateSimpleProgram() { var pgm = new Program();