From aae593a73ea32f65d08b9fac3be585b228c98e29 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Thu, 2 Apr 2026 11:08:40 -0400 Subject: [PATCH] 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) --- .../CincinnatiFeatureWriter.cs | 49 ++++++++++++-- .../CincinnatiSheetWriter.cs | 16 +++-- .../Cincinnati/UserVariablePostTests.cs | 65 +++++++++++++++++++ 3 files changed, 122 insertions(+), 8 deletions(-) diff --git a/OpenNest.Posts.Cincinnati/CincinnatiFeatureWriter.cs b/OpenNest.Posts.Cincinnati/CincinnatiFeatureWriter.cs index 18ff323..c2777a6 100644 --- a/OpenNest.Posts.Cincinnati/CincinnatiFeatureWriter.cs +++ b/OpenNest.Posts.Cincinnati/CincinnatiFeatureWriter.cs @@ -40,6 +40,18 @@ public sealed class FeatureContext /// The drawing ID for the current part, used to look up user variable mappings. /// public int DrawingId { get; set; } + + /// + /// True if this feature is a cut-off line. Used to substitute plate-edge + /// coordinates with sheet width/length variables. + /// + public bool IsCutOff { get; set; } + + /// Plate width (Y extent for vertical cutoffs). + public double PlateWidth { get; set; } + + /// Plate length (X extent for horizontal cutoffs). + public double PlateLength { get; set; } } /// @@ -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 /// /// 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. /// private string FormatCoordWithVars(double value, string axis, Dictionary 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); } + /// + /// For cut-off coordinates, checks if the value matches a plate edge dimension + /// and returns the sheet variable reference (e.g., "#110") if so. + /// + 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 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()); } diff --git a/OpenNest.Posts.Cincinnati/CincinnatiSheetWriter.cs b/OpenNest.Posts.Cincinnati/CincinnatiSheetWriter.cs index 87b5449..63228e4 100644 --- a/OpenNest.Posts.Cincinnati/CincinnatiSheetWriter.cs +++ b/OpenNest.Posts.Cincinnati/CincinnatiSheetWriter.cs @@ -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 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 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); diff --git a/OpenNest.Tests/Cincinnati/UserVariablePostTests.cs b/OpenNest.Tests/Cincinnati/UserVariablePostTests.cs index cb72170..5c5db2e 100644 --- a/OpenNest.Tests/Cincinnati/UserVariablePostTests.cs +++ b/OpenNest.Tests/Cincinnati/UserVariablePostTests.cs @@ -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);