feat: Cincinnati post emits user variables as numbered #variables
When programs have user-defined variables, the Cincinnati post now: - Assigns numbered machine variables (#200, #201, etc.) to non-inline variables - Emits declarations like #200=48.0 (SHEET WIDTH) in the variable declaration subprogram - Emits X#200 instead of X48.0 in coordinates that have VariableRefs - Handles global variables (shared number across drawings) vs local (per-drawing number) - Inline variables emit the literal value as before Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -29,6 +29,17 @@ public sealed class FeatureContext
|
||||
/// so part-relative programs become plate-absolute under G90.
|
||||
/// </summary>
|
||||
public Vector PartLocation { get; set; } = Vector.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// Maps (drawingId, variableName) to assigned machine variable numbers.
|
||||
/// Used to emit #number references instead of literal values for user variables.
|
||||
/// </summary>
|
||||
public Dictionary<(int drawingId, string varName), int> UserVariableMapping { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The drawing ID for the current part, used to look up user variable mappings.
|
||||
/// </summary>
|
||||
public int DrawingId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -112,7 +123,9 @@ public sealed class CincinnatiFeatureWriter
|
||||
kerfEmitted = true;
|
||||
}
|
||||
|
||||
sb.Append($"G1 X{_fmt.FormatCoord(linear.EndPoint.X + offset.X)} Y{_fmt.FormatCoord(linear.EndPoint.Y + offset.Y)}");
|
||||
var xCoord = FormatCoordWithVars(linear.EndPoint.X + offset.X, "X", linear.VariableRefs, ctx);
|
||||
var yCoord = FormatCoordWithVars(linear.EndPoint.Y + offset.Y, "Y", linear.VariableRefs, ctx);
|
||||
sb.Append($"G1 X{xCoord} Y{yCoord}");
|
||||
|
||||
// Feedrate — etch always uses process feedrate
|
||||
var feedVar = ctx.IsEtch ? "#148" : GetLinearFeedVariable(linear.Layer);
|
||||
@@ -138,7 +151,9 @@ 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 + offset.X)} Y{_fmt.FormatCoord(arc.EndPoint.Y + offset.Y)}");
|
||||
var xCoord = FormatCoordWithVars(arc.EndPoint.X + offset.X, "X", arc.VariableRefs, ctx);
|
||||
var yCoord = FormatCoordWithVars(arc.EndPoint.Y + offset.Y, "Y", arc.VariableRefs, ctx);
|
||||
sb.Append($"{gCode} X{xCoord} Y{yCoord}");
|
||||
|
||||
// Convert absolute center to incremental I/J
|
||||
var i = arc.CenterPoint.X - currentPos.X;
|
||||
@@ -177,6 +192,25 @@ public sealed class CincinnatiFeatureWriter
|
||||
WriteM47(writer, ctx);
|
||||
}
|
||||
|
||||
/// <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).
|
||||
/// Inline variables fall through to literal formatting.
|
||||
/// </summary>
|
||||
private string FormatCoordWithVars(double value, string axis,
|
||||
Dictionary<string, string> variableRefs, FeatureContext ctx)
|
||||
{
|
||||
if (variableRefs != null
|
||||
&& variableRefs.TryGetValue(axis, out var varName)
|
||||
&& ctx.UserVariableMapping != null
|
||||
&& ctx.UserVariableMapping.TryGetValue((ctx.DrawingId, varName), out var varNum))
|
||||
{
|
||||
return $"#{varNum}";
|
||||
}
|
||||
|
||||
return _fmt.FormatCoord(value);
|
||||
}
|
||||
|
||||
private Vector FindPiercePoint(List<ICode> codes)
|
||||
{
|
||||
foreach (var code in codes)
|
||||
|
||||
@@ -253,6 +253,11 @@ namespace OpenNest.Posts.Cincinnati
|
||||
new() { MaxRadius = 4.500, FeedratePercent = 0.80, VariableNumber = 125 }
|
||||
};
|
||||
|
||||
[Category("A. Variables")]
|
||||
[DisplayName("User Variable Start")]
|
||||
[Description("Starting variable number for user-defined variables (#200, #201, etc.).")]
|
||||
public int UserVariableStart { get; set; } = 200;
|
||||
|
||||
[Category("A. Variables")]
|
||||
[DisplayName("Sheet Width Variable")]
|
||||
[Description("Variable number for sheet width.")]
|
||||
|
||||
@@ -70,7 +70,10 @@ namespace OpenNest.Posts.Cincinnati
|
||||
.Where(p => p.Parts.Count > 0)
|
||||
.ToList();
|
||||
|
||||
// 3. Resolve gas and library files
|
||||
// 3. Register user variables from drawing programs
|
||||
var userVarMapping = RegisterUserVariables(vars, plates);
|
||||
|
||||
// 4. Resolve gas and library files
|
||||
var resolver = new MaterialLibraryResolver(Config);
|
||||
var gas = MaterialLibraryResolver.ResolveGas(nest, Config);
|
||||
var etchLibrary = resolver.ResolveEtchLibrary(Config.DefaultEtchGas);
|
||||
@@ -79,24 +82,24 @@ namespace OpenNest.Posts.Cincinnati
|
||||
var firstPlate = plates.FirstOrDefault();
|
||||
var initialCutLibrary = resolver.ResolveCutLibrary(nest.Material?.Name ?? "", nest.Thickness, gas);
|
||||
|
||||
// 4. Build part sub-program registry (if enabled)
|
||||
// 5. Build part sub-program registry (if enabled)
|
||||
Dictionary<(int, long), int> partSubprograms = null;
|
||||
List<(int subNum, string name, Program program)> subprogramEntries = null;
|
||||
|
||||
if (Config.UsePartSubprograms)
|
||||
(partSubprograms, subprogramEntries) = CincinnatiPartSubprogramWriter.BuildRegistry(plates, Config.PartSubprogramStart);
|
||||
|
||||
// 5. Create writers
|
||||
// 6. Create writers
|
||||
var preamble = new CincinnatiPreambleWriter(Config);
|
||||
var sheetWriter = new CincinnatiSheetWriter(Config, vars);
|
||||
|
||||
// 6. Build material description from nest
|
||||
// 7. Build material description from nest
|
||||
var material = nest.Material;
|
||||
var materialDesc = material != null
|
||||
? $"{material.Name}{(string.IsNullOrEmpty(material.Grade) ? "" : $", {material.Grade}")}"
|
||||
: "";
|
||||
|
||||
// 7. Write to stream
|
||||
// 8. Write to stream
|
||||
using var writer = new StreamWriter(outputStream, Encoding.UTF8, 1024, leaveOpen: true);
|
||||
|
||||
// Main program
|
||||
@@ -114,7 +117,7 @@ namespace OpenNest.Posts.Cincinnati
|
||||
var cutLibrary = resolver.ResolveCutLibrary(nest.Material?.Name ?? "", nest.Thickness, gas);
|
||||
var isLastSheet = i == plates.Count - 1;
|
||||
sheetWriter.Write(writer, plate, nest.Name ?? "NEST", sheetIndex, subNumber,
|
||||
cutLibrary, etchLibrary, partSubprograms, isLastSheet);
|
||||
cutLibrary, etchLibrary, partSubprograms, isLastSheet, userVarMapping);
|
||||
}
|
||||
|
||||
// Part sub-programs (if enabled)
|
||||
@@ -142,6 +145,103 @@ namespace OpenNest.Posts.Cincinnati
|
||||
Post(nest, fs);
|
||||
}
|
||||
|
||||
private Dictionary<(int drawingId, string varName), int> RegisterUserVariables(
|
||||
ProgramVariableManager vars, List<Plate> plates)
|
||||
{
|
||||
var mapping = new Dictionary<(int drawingId, string varName), int>();
|
||||
var nextNumber = Config.UserVariableStart;
|
||||
|
||||
// Track global variables by name so they share a single number
|
||||
var globalNumbers = new Dictionary<string, int>(System.StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// Collect unique drawings from all plates
|
||||
var seenDrawings = new HashSet<int>();
|
||||
foreach (var plate in plates)
|
||||
{
|
||||
foreach (var part in plate.Parts)
|
||||
{
|
||||
var drawing = part.BaseDrawing;
|
||||
if (drawing.IsCutOff || !seenDrawings.Add(drawing.Id))
|
||||
continue;
|
||||
|
||||
foreach (var kvp in drawing.Program.Variables)
|
||||
{
|
||||
var varDef = kvp.Value;
|
||||
|
||||
// Skip inline variables — they emit literal values
|
||||
if (varDef.Inline)
|
||||
continue;
|
||||
|
||||
if (varDef.Global)
|
||||
{
|
||||
if (!globalNumbers.TryGetValue(varDef.Name, out var globalNum))
|
||||
{
|
||||
globalNum = nextNumber++;
|
||||
globalNumbers[varDef.Name] = globalNum;
|
||||
|
||||
// Register once in the variable manager
|
||||
var commentName = ToPascalCase(varDef.Name);
|
||||
var expression = FormatVariableValue(varDef.Value);
|
||||
vars.GetOrCreate(commentName, globalNum, expression);
|
||||
}
|
||||
|
||||
mapping[(drawing.Id, varDef.Name)] = globalNum;
|
||||
}
|
||||
else
|
||||
{
|
||||
var num = nextNumber++;
|
||||
mapping[(drawing.Id, varDef.Name)] = num;
|
||||
|
||||
// Register with drawing name prefix in the comment
|
||||
var drawingLabel = ToPascalCase(drawing.Name);
|
||||
var varLabel = ToPascalCase(varDef.Name);
|
||||
var commentName = $"{drawingLabel}{varLabel}";
|
||||
var expression = FormatVariableValue(varDef.Value);
|
||||
vars.GetOrCreate(commentName, num, expression);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mapping;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a variable name from snake_case or camelCase to PascalCase.
|
||||
/// Examples: "sheet_width" → "SheetWidth", "holeSpacing" → "HoleSpacing"
|
||||
/// </summary>
|
||||
private static string ToPascalCase(string name)
|
||||
{
|
||||
var sb = new StringBuilder(name.Length);
|
||||
var capitalizeNext = true;
|
||||
|
||||
foreach (var c in name)
|
||||
{
|
||||
if (c == '_')
|
||||
{
|
||||
capitalizeNext = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (capitalizeNext)
|
||||
{
|
||||
sb.Append(char.ToUpper(c));
|
||||
capitalizeNext = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append(c);
|
||||
}
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string FormatVariableValue(double value)
|
||||
{
|
||||
return value.ToString("0.####", System.Globalization.CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
private ProgramVariableManager CreateVariableManager()
|
||||
{
|
||||
var vars = new ProgramVariableManager();
|
||||
|
||||
@@ -38,7 +38,8 @@ public sealed class CincinnatiSheetWriter
|
||||
public void Write(TextWriter w, Plate plate, string nestName, int sheetIndex, int subNumber,
|
||||
string cutLibrary, string etchLibrary,
|
||||
Dictionary<(int, long), int> partSubprograms = null,
|
||||
bool isLastSheet = false)
|
||||
bool isLastSheet = false,
|
||||
Dictionary<(int drawingId, string varName), int> userVarMapping = null)
|
||||
{
|
||||
if (plate.Parts.Count == 0)
|
||||
return;
|
||||
@@ -88,9 +89,9 @@ public sealed class CincinnatiSheetWriter
|
||||
|
||||
// 4. Emit parts
|
||||
if (partSubprograms != null)
|
||||
WritePartsWithSubprograms(w, allParts, cutLibrary, etchLibrary, sheetDiagonal, partSubprograms);
|
||||
WritePartsWithSubprograms(w, allParts, cutLibrary, etchLibrary, sheetDiagonal, partSubprograms, userVarMapping);
|
||||
else
|
||||
WritePartsInline(w, allParts, cutLibrary, etchLibrary, sheetDiagonal);
|
||||
WritePartsInline(w, allParts, cutLibrary, etchLibrary, sheetDiagonal, userVarMapping);
|
||||
|
||||
// 5. Footer
|
||||
w.WriteLine("M42");
|
||||
@@ -104,7 +105,8 @@ public sealed class CincinnatiSheetWriter
|
||||
|
||||
private void WritePartsWithSubprograms(TextWriter w, List<Part> allParts,
|
||||
string cutLibrary, string etchLibrary, double sheetDiagonal,
|
||||
Dictionary<(int, long), int> partSubprograms)
|
||||
Dictionary<(int, long), int> partSubprograms,
|
||||
Dictionary<(int drawingId, string varName), int> userVarMapping)
|
||||
{
|
||||
var lastPartName = "";
|
||||
var featureIndex = 0;
|
||||
@@ -154,7 +156,9 @@ public sealed class CincinnatiSheetWriter
|
||||
LibraryFile = isEtch ? etchLibrary : cutLibrary,
|
||||
CutDistance = cutDistance,
|
||||
SheetDiagonal = sheetDiagonal,
|
||||
PartLocation = part.Location
|
||||
PartLocation = part.Location,
|
||||
UserVariableMapping = userVarMapping,
|
||||
DrawingId = part.BaseDrawing.Id
|
||||
};
|
||||
|
||||
_featureWriter.Write(w, ctx);
|
||||
@@ -202,7 +206,8 @@ public sealed class CincinnatiSheetWriter
|
||||
}
|
||||
|
||||
private void WritePartsInline(TextWriter w, List<Part> allParts,
|
||||
string cutLibrary, string etchLibrary, double sheetDiagonal)
|
||||
string cutLibrary, string etchLibrary, double sheetDiagonal,
|
||||
Dictionary<(int drawingId, string varName), int> userVarMapping)
|
||||
{
|
||||
// Split and classify features, ordering etch before cut per part
|
||||
var features = new List<(Part part, List<ICode> codes, bool isEtch)>();
|
||||
@@ -242,7 +247,9 @@ public sealed class CincinnatiSheetWriter
|
||||
LibraryFile = isEtch ? etchLibrary : cutLibrary,
|
||||
CutDistance = cutDistance,
|
||||
SheetDiagonal = sheetDiagonal,
|
||||
PartLocation = part.Location
|
||||
PartLocation = part.Location,
|
||||
UserVariableMapping = userVarMapping,
|
||||
DrawingId = part.BaseDrawing.Id
|
||||
};
|
||||
|
||||
_featureWriter.Write(w, ctx);
|
||||
|
||||
142
OpenNest.Tests/Cincinnati/UserVariablePostTests.cs
Normal file
142
OpenNest.Tests/Cincinnati/UserVariablePostTests.cs
Normal file
@@ -0,0 +1,142 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.IO;
|
||||
using OpenNest.Posts.Cincinnati;
|
||||
|
||||
namespace OpenNest.Tests.Cincinnati;
|
||||
|
||||
public class UserVariablePostTests
|
||||
{
|
||||
[Fact]
|
||||
public void UserVariables_EmittedInDeclarationSubprogram()
|
||||
{
|
||||
var output = PostNestWithVariables("width = 48.0\nG90\nG01X$widthY0");
|
||||
|
||||
Assert.Contains("#200=48", output);
|
||||
Assert.Contains("WIDTH", output.ToUpper());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UserVariables_InlineVariable_NotEmittedAsNumbered()
|
||||
{
|
||||
var output = PostNestWithVariables("kerf = 0.06 inline\nG90\nG01X1Y0");
|
||||
|
||||
Assert.DoesNotContain("#200", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UserVariables_CoordinateUsesNumberedVariable()
|
||||
{
|
||||
var output = PostNestWithVariables("width = 48.0\nG90\nG01X$widthY0");
|
||||
|
||||
Assert.Contains("X#200", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UserVariables_InlineVariable_CoordinateUsesLiteral()
|
||||
{
|
||||
var output = PostNestWithVariables("kerf = 0.06 inline\nG90\nG01X$kerfY0");
|
||||
|
||||
Assert.Contains("X0.06", output);
|
||||
// G1 coordinate lines should not use X#nnn variable references for inline vars
|
||||
var g1Lines = output.Split('\n').Where(l => l.TrimStart().StartsWith("G1 ")).ToList();
|
||||
Assert.All(g1Lines, line => Assert.DoesNotContain("X#", line));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UserVariables_GlobalVariables_SharedAcrossDrawings()
|
||||
{
|
||||
var pgm1 = ParseProgram("sheet_width = 48.0 global\nG90\nG01X$sheet_widthY0");
|
||||
var pgm2 = ParseProgram("sheet_width = 48.0 global\nG90\nG01X$sheet_widthY0");
|
||||
|
||||
var drawing1 = new Drawing("Part1", pgm1);
|
||||
var drawing2 = new Drawing("Part2", pgm2);
|
||||
var nest = new Nest { Name = "Test" };
|
||||
nest.Drawings.Add(drawing1);
|
||||
nest.Drawings.Add(drawing2);
|
||||
var plate = new Plate(new Size(100, 100));
|
||||
plate.Parts.Add(new Part(drawing1, new Vector(0, 0)));
|
||||
plate.Parts.Add(new Part(drawing2, new Vector(50, 0)));
|
||||
nest.Plates.Add(plate);
|
||||
|
||||
var config = new CincinnatiPostConfig { UserVariableStart = 200 };
|
||||
var post = new CincinnatiPostProcessor(config);
|
||||
var output = PostToString(post, nest);
|
||||
|
||||
// Both should use the same #200 — only one declaration
|
||||
var declarationCount = output.Split('\n')
|
||||
.Count(l => l.Contains("#200=") && l.ToUpper().Contains("SHEET WIDTH"));
|
||||
Assert.Equal(1, declarationCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UserVariables_LocalVariables_GetSeparateNumbers()
|
||||
{
|
||||
var pgm1 = ParseProgram("diameter = 0.3\nG90\nG01X$diameterY0");
|
||||
var pgm2 = ParseProgram("diameter = 0.5\nG90\nG01X$diameterY0");
|
||||
|
||||
var drawing1 = new Drawing("TubeA", pgm1);
|
||||
var drawing2 = new Drawing("TubeB", pgm2);
|
||||
var nest = new Nest { Name = "Test" };
|
||||
nest.Drawings.Add(drawing1);
|
||||
nest.Drawings.Add(drawing2);
|
||||
var plate = new Plate(new Size(100, 100));
|
||||
plate.Parts.Add(new Part(drawing1, new Vector(0, 0)));
|
||||
plate.Parts.Add(new Part(drawing2, new Vector(50, 0)));
|
||||
nest.Plates.Add(plate);
|
||||
|
||||
var config = new CincinnatiPostConfig { UserVariableStart = 200 };
|
||||
var post = new CincinnatiPostProcessor(config);
|
||||
var output = PostToString(post, nest);
|
||||
|
||||
// Two separate declarations with different numbers
|
||||
Assert.Contains("#200=0.3", output);
|
||||
Assert.Contains("#201=0.5", output);
|
||||
Assert.Contains("TUBE A", output.ToUpper());
|
||||
Assert.Contains("TUBE B", output.ToUpper());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UserVariables_StartNumberConfigurable()
|
||||
{
|
||||
var config = new CincinnatiPostConfig { UserVariableStart = 300 };
|
||||
var output = PostNestWithVariables("width = 48.0\nG90\nG01X$widthY0", config);
|
||||
|
||||
Assert.Contains("#300=48", output);
|
||||
}
|
||||
|
||||
private static string PostNestWithVariables(string gcode, CincinnatiPostConfig config = null)
|
||||
{
|
||||
var program = ParseProgram(gcode);
|
||||
var drawing = new Drawing("TestPart", program);
|
||||
var nest = new Nest { Name = "Test" };
|
||||
nest.Drawings.Add(drawing);
|
||||
var plate = new Plate(new Size(100, 100));
|
||||
plate.Parts.Add(new Part(drawing, new Vector(0, 0)));
|
||||
nest.Plates.Add(plate);
|
||||
|
||||
config ??= new CincinnatiPostConfig { UserVariableStart = 200 };
|
||||
var post = new CincinnatiPostProcessor(config);
|
||||
return PostToString(post, nest);
|
||||
}
|
||||
|
||||
private static string PostToString(CincinnatiPostProcessor post, Nest nest)
|
||||
{
|
||||
var ms = new MemoryStream();
|
||||
post.Post(nest, ms);
|
||||
ms.Position = 0;
|
||||
return new StreamReader(ms).ReadToEnd();
|
||||
}
|
||||
|
||||
private static Program ParseProgram(string gcode)
|
||||
{
|
||||
var stream = new MemoryStream(Encoding.UTF8.GetBytes(gcode));
|
||||
var reader = new ProgramReader(stream);
|
||||
var program = reader.Read();
|
||||
reader.Close();
|
||||
return program;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user