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) <noreply@anthropic.com>
This commit is contained in:
2026-03-31 09:50:43 -04:00
parent d7fa4bef43
commit 568539d5b1
3 changed files with 77 additions and 7 deletions

View File

@@ -23,6 +23,12 @@ public sealed class FeatureContext
public string LibraryFile { get; set; } = "";
public double CutDistance { get; set; }
public double SheetDiagonal { get; set; }
/// <summary>
/// Part location on the plate. Added to all output X/Y coordinates
/// so part-relative programs become plate-absolute under G90.
/// </summary>
public Vector PartLocation { get; set; } = Vector.Zero;
}
/// <summary>
@@ -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());
}

View File

@@ -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);

View File

@@ -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();