feat: cutoff coordinates use sheet width/length variables in Cincinnati post

Cutoff features now substitute plate-edge coordinates with #SheetWidthVariable
and #SheetLengthVariable references. Vertical cutoffs at Y=plate_width emit
Y#110, horizontal cutoffs at X=plate_length emit X#111. Segmented cutoffs
only substitute the edge coordinate, interior segment endpoints stay literal.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-02 11:08:40 -04:00
parent 36d8f7fb11
commit aae593a73e
3 changed files with 122 additions and 8 deletions

View File

@@ -40,6 +40,18 @@ public sealed class FeatureContext
/// The drawing ID for the current part, used to look up user variable mappings.
/// </summary>
public int DrawingId { get; set; }
/// <summary>
/// True if this feature is a cut-off line. Used to substitute plate-edge
/// coordinates with sheet width/length variables.
/// </summary>
public bool IsCutOff { get; set; }
/// <summary>Plate width (Y extent for vertical cutoffs).</summary>
public double PlateWidth { get; set; }
/// <summary>Plate length (X extent for horizontal cutoffs).</summary>
public double PlateLength { get; set; }
}
/// <summary>
@@ -74,7 +86,7 @@ public sealed class CincinnatiFeatureWriter
var piercePoint = FindPiercePoint(ctx.Codes);
// 1. Rapid to pierce point (with line number if configured)
WriteRapidToPierce(writer, ctx.FeatureNumber, piercePoint, offset);
WriteRapidToPierce(writer, ctx, piercePoint, offset);
// 2. Part name comment on first feature of each part
if (ctx.IsFirstFeatureOfPart && !string.IsNullOrEmpty(ctx.PartName))
@@ -195,11 +207,14 @@ public sealed class CincinnatiFeatureWriter
/// <summary>
/// Formats a coordinate value, using a #number variable reference if the motion
/// has a VariableRef for this axis and the variable is mapped (non-inline).
/// For cut-off features, plate-edge coordinates are substituted with
/// the sheet width/length variables.
/// Inline variables fall through to literal formatting.
/// </summary>
private string FormatCoordWithVars(double value, string axis,
Dictionary<string, string> variableRefs, FeatureContext ctx)
{
// User-defined variable references take priority
if (variableRefs != null
&& variableRefs.TryGetValue(axis, out var varName)
&& ctx.UserVariableMapping != null
@@ -208,9 +223,33 @@ public sealed class CincinnatiFeatureWriter
return $"#{varNum}";
}
// Cut-off plate-edge substitution
if (ctx.IsCutOff)
{
var sheetVar = MatchCutOffSheetVariable(value, axis, ctx);
if (sheetVar != null)
return sheetVar;
}
return _fmt.FormatCoord(value);
}
/// <summary>
/// For cut-off coordinates, checks if the value matches a plate edge dimension
/// and returns the sheet variable reference (e.g., "#110") if so.
/// </summary>
private string MatchCutOffSheetVariable(double value, string axis, FeatureContext ctx)
{
// Vertical cutoffs travel along Y — the Y endpoint at the plate edge = sheet width
// Horizontal cutoffs travel along X — the X endpoint at the plate edge = sheet length
if (axis == "Y" && Tolerance.IsEqualTo(value, ctx.PlateWidth))
return $"#{_config.SheetWidthVariable}";
if (axis == "X" && Tolerance.IsEqualTo(value, ctx.PlateLength))
return $"#{_config.SheetLengthVariable}";
return null;
}
private Vector FindPiercePoint(List<ICode> codes)
{
foreach (var code in codes)
@@ -229,14 +268,16 @@ public sealed class CincinnatiFeatureWriter
return Vector.Zero;
}
private void WriteRapidToPierce(TextWriter writer, int featureNumber, Vector piercePoint, Vector offset)
private void WriteRapidToPierce(TextWriter writer, FeatureContext ctx, Vector piercePoint, Vector offset)
{
var sb = new StringBuilder();
if (_config.UseLineNumbers)
sb.Append($"N{featureNumber} ");
sb.Append($"N{ctx.FeatureNumber} ");
sb.Append($"G0 X{_fmt.FormatCoord(piercePoint.X + offset.X)} Y{_fmt.FormatCoord(piercePoint.Y + offset.Y)}");
var xCoord = FormatCoordWithVars(piercePoint.X + offset.X, "X", null, ctx);
var yCoord = FormatCoordWithVars(piercePoint.Y + offset.Y, "Y", null, ctx);
sb.Append($"G0 X{xCoord} Y{yCoord}");
writer.WriteLine(sb.ToString());
}

View File

@@ -89,9 +89,9 @@ public sealed class CincinnatiSheetWriter
// 4. Emit parts
if (partSubprograms != null)
WritePartsWithSubprograms(w, allParts, cutLibrary, etchLibrary, sheetDiagonal, partSubprograms, userVarMapping);
WritePartsWithSubprograms(w, allParts, cutLibrary, etchLibrary, sheetDiagonal, width, length, partSubprograms, userVarMapping);
else
WritePartsInline(w, allParts, cutLibrary, etchLibrary, sheetDiagonal, userVarMapping);
WritePartsInline(w, allParts, cutLibrary, etchLibrary, sheetDiagonal, width, length, userVarMapping);
// 5. Footer
w.WriteLine("M42");
@@ -105,6 +105,7 @@ public sealed class CincinnatiSheetWriter
private void WritePartsWithSubprograms(TextWriter w, List<Part> allParts,
string cutLibrary, string etchLibrary, double sheetDiagonal,
double plateWidth, double plateLength,
Dictionary<(int, long), int> partSubprograms,
Dictionary<(int drawingId, string varName), int> userVarMapping)
{
@@ -158,7 +159,10 @@ public sealed class CincinnatiSheetWriter
SheetDiagonal = sheetDiagonal,
PartLocation = part.Location,
UserVariableMapping = userVarMapping,
DrawingId = part.BaseDrawing.Id
DrawingId = part.BaseDrawing.Id,
IsCutOff = part.BaseDrawing.IsCutOff,
PlateWidth = plateWidth,
PlateLength = plateLength
};
_featureWriter.Write(w, ctx);
@@ -207,6 +211,7 @@ public sealed class CincinnatiSheetWriter
private void WritePartsInline(TextWriter w, List<Part> allParts,
string cutLibrary, string etchLibrary, double sheetDiagonal,
double plateWidth, double plateLength,
Dictionary<(int drawingId, string varName), int> userVarMapping)
{
// Split and classify features, ordering etch before cut per part
@@ -249,7 +254,10 @@ public sealed class CincinnatiSheetWriter
SheetDiagonal = sheetDiagonal,
PartLocation = part.Location,
UserVariableMapping = userVarMapping,
DrawingId = part.BaseDrawing.Id
DrawingId = part.BaseDrawing.Id,
IsCutOff = part.BaseDrawing.IsCutOff,
PlateWidth = plateWidth,
PlateLength = plateLength
};
_featureWriter.Write(w, ctx);

View File

@@ -108,6 +108,71 @@ public class UserVariablePostTests
Assert.Contains("#300=48", output);
}
[Fact]
public void CutOff_VerticalCut_UsesSheetWidthVariable()
{
// Create a plate with a vertical cutoff
var config = new CincinnatiPostConfig { SheetWidthVariable = 110, SheetLengthVariable = 111 };
var nest = new Nest { Name = "Test" };
var plate = new Plate(new Size(48, 96));
// Add a simple part so the plate isn't empty
var partPgm = new Program();
partPgm.Codes.Add(new RapidMove(0, 0));
partPgm.Codes.Add(new LinearMove(10, 0));
partPgm.Codes.Add(new LinearMove(10, 10));
partPgm.Codes.Add(new LinearMove(0, 10));
partPgm.Codes.Add(new LinearMove(0, 0));
var drawing = new Drawing("Part1", partPgm);
nest.Drawings.Add(drawing);
plate.Parts.Add(new Part(drawing, new Vector(0, 0)));
// Add a vertical cutoff that goes full width (Y=0 to Y=48)
var cutoff = new CutOff(new Vector(20, 0), CutOffAxis.Vertical);
plate.CutOffs.Add(cutoff);
plate.RegenerateCutOffs(new CutOffSettings());
nest.Plates.Add(plate);
var post = new CincinnatiPostProcessor(config);
var output = PostToString(post, nest);
// The cutoff line end at Y=48 (sheet width) should use #110
Assert.Contains("Y#110", output);
}
[Fact]
public void CutOff_SegmentedCut_OnlyEdgeUsesVariable()
{
// Create a plate with a part in the middle and a vertical cutoff
var config = new CincinnatiPostConfig { SheetWidthVariable = 110 };
var nest = new Nest { Name = "Test" };
var plate = new Plate(new Size(48, 96));
// Part in the middle — cutoff will be segmented around it
var partPgm = new Program();
partPgm.Codes.Add(new RapidMove(0, 0));
partPgm.Codes.Add(new LinearMove(10, 0));
partPgm.Codes.Add(new LinearMove(10, 10));
partPgm.Codes.Add(new LinearMove(0, 10));
partPgm.Codes.Add(new LinearMove(0, 0));
var drawing = new Drawing("Part1", partPgm);
nest.Drawings.Add(drawing);
plate.Parts.Add(new Part(drawing, new Vector(15, 20))); // Part at Y=20-30, should create gap
var cutoff = new CutOff(new Vector(20, 0), CutOffAxis.Vertical);
plate.CutOffs.Add(cutoff);
plate.RegenerateCutOffs(new CutOffSettings());
nest.Plates.Add(plate);
var post = new CincinnatiPostProcessor(config);
var output = PostToString(post, nest);
// The last segment endpoint at Y=48 should use #110
Assert.Contains("Y#110", output);
}
private static string PostNestWithVariables(string gcode, CincinnatiPostConfig config = null)
{
var program = ParseProgram(gcode);