Compare commits
10
Commits
95b9613e2d
...
5bcad9667b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5bcad9667b | ||
|
|
64945220b9 | ||
|
|
ec0baad585 | ||
|
|
f26edb824d | ||
|
|
aae593a73e | ||
|
|
36d8f7fb11 | ||
|
|
52ad5b4575 | ||
|
|
7416f8ae3f | ||
|
|
46e3104dfc | ||
|
|
27afa04e4a |
@@ -24,10 +24,10 @@ Eight projects form a layered architecture:
|
||||
Domain model, geometry, and CNC primitives organized into namespaces:
|
||||
|
||||
- **Root** (`namespace OpenNest`): Domain model — `Nest` → `Plate[]` → `Part[]` → `Drawing` → `Program`. A `Nest` is the top-level container. Each `Plate` has a size, material, quadrant, spacing, and contains placed `Part` instances. Each `Part` references a `Drawing` (the template) and has its own location/rotation. A `Drawing` wraps a CNC `Program`. Also contains utilities: `PartGeometry`, `Align`, `Sequence`, `Timing`.
|
||||
- **CNC** (`CNC/`, `namespace OpenNest.CNC`): `Program` holds a list of `ICode` instructions (G-code-like: `RapidMove`, `LinearMove`, `ArcMove`, `SubProgramCall`). Programs support absolute/incremental mode conversion, rotation, offset, bounding box calculation, and cloning.
|
||||
- **CNC** (`CNC/`, `namespace OpenNest.CNC`): `Program` holds a list of `ICode` instructions (G-code-like: `RapidMove`, `LinearMove`, `ArcMove`, `SubProgramCall`) and an optional `Variables` dictionary of `VariableDefinition` entries. Programs support absolute/incremental mode conversion, rotation, offset, bounding box calculation, and cloning. `VariableDefinition` stores a named variable's expression, resolved value, and flags (`Inline`, `Global`). `ProgramVariableManager` manages numbered machine variables for post-processor output.
|
||||
- **Geometry** (`Geometry/`, `namespace OpenNest.Geometry`): Spatial primitives (`Vector`, `Box`, `Size`, `Spacing`, `BoundingBox`, `IBoundable`) and higher-level shapes (`Line`, `Arc`, `Circle`, `Polygon`, `Shape`) used for intersection detection, area calculation, and DXF conversion. Also contains `Intersect` (intersection algorithms), `ShapeBuilder` (entity chaining), `GeometryOptimizer` (line/arc merging), `SpatialQuery` (directional distance, ray casting, box queries), `ShapeProfile` (perimeter/area analysis), `NoFitPolygon`, `InnerFitPolygon`, `ConvexHull`, `ConvexDecomposition`, `RotatingCalipers`, and `Collision` (overlap detection with Sutherland-Hodgman polygon clipping and hole subtraction).
|
||||
- **Converters** (`Converters/`, `namespace OpenNest.Converters`): Bridges between CNC and Geometry — `ConvertProgram` (CNC→Geometry), `ConvertGeometry` (Geometry→CNC), `ConvertMode` (absolute↔incremental).
|
||||
- **Math** (`Math/`, `namespace OpenNest.Math`): `Angle` (radian/degree conversion), `Tolerance` (floating-point comparison), `Trigonometry`, `Generic` (swap utility), `EvenOdd`, `Rounding` (factor-based rounding). Note: `OpenNest.Math` shadows `System.Math` — use `System.Math` fully qualified where both are needed.
|
||||
- **Math** (`Math/`, `namespace OpenNest.Math`): `Angle` (radian/degree conversion), `Tolerance` (floating-point comparison), `Trigonometry`, `Generic` (swap utility), `EvenOdd`, `Rounding` (factor-based rounding), `ExpressionEvaluator` (arithmetic expression parser for G-code variable expressions with `$name` references). Note: `OpenNest.Math` shadows `System.Math` — use `System.Math` fully qualified where both are needed.
|
||||
- **CNC/CuttingStrategy** (`CNC/CuttingStrategy/`, `namespace OpenNest.CNC`): `ContourCuttingStrategy` orchestrates cut ordering, lead-ins/lead-outs, and tabs. Includes `LeadIn`/`LeadOut` hierarchies (line, arc, clean-hole variants), `Tab` hierarchy (normal, machine, breaker), and `CuttingParameters`/`AssignmentParameters`/`SequenceParameters` configuration.
|
||||
- **Collections** (`Collections/`, `namespace OpenNest.Collections`): `ObservableList<T>`, `DrawingCollection`.
|
||||
- **CutOffs** (`namespace OpenNest`): `CutOff` (axis-aligned cut line with position, axis, optional start/end limits), `CutOffAxis` enum (`Horizontal`, `Vertical`), `CutOffSettings` (clearance, overtravel, min segment length, direction), `CutDirection` enum (`TowardOrigin`, `AwayFromOrigin`). Cut-offs generate CNC `Program` objects with trimmed line segments that avoid parts.
|
||||
@@ -116,3 +116,4 @@ Always keep `README.md` and `CLAUDE.md` up to date when making changes that affe
|
||||
- `Compactor` performs post-fill gravity compaction — after filling, parts are pushed toward a plate edge using directional distance calculations to close gaps between irregular shapes.
|
||||
- `FillScore` uses lexicographic comparison (count > utilization > compactness) to rank fill results consistently across all fill strategies.
|
||||
- **Cut-off materialization lifecycle**: `CutOff` objects live on `Plate.CutOffs`. Each generates a `Drawing` (with `IsCutOff = true`) whose `Program` contains trimmed line segments. `Plate.RegenerateCutOffs(settings)` removes old cut-off Parts, recomputes programs, and re-adds them to `Plate.Parts`. Regeneration triggers: cut-off add/remove/move, part drag complete, fill complete, plate transform. Cut-off Parts are excluded from quantity tracking, utilization, overlap detection, and nest file serialization (programs are regenerated from definitions on load).
|
||||
- **User-defined G-code variables**: Programs can contain named variable definitions (`name = expression [inline] [global]`) referenced in coordinates with `$name`. Variables resolve to doubles at parse time for geometry/nesting. `VariableRefs` on `Motion`/`Feedrate` track the symbolic link so post processors can emit machine variable references. Cincinnati post maps non-inline variables to numbered machine variables (`#200+`) with descriptive comments. Global variables share a number across programs; local variables get per-drawing numbers. `ProgramReader` uses a two-pass parse (collect definitions, then parse G-code with substitution). `NestWriter` serializes definitions and `$references` back to text for round-trip fidelity.
|
||||
|
||||
@@ -70,8 +70,8 @@ namespace OpenNest.CNC.CuttingStrategy
|
||||
private void EmitContour(Program program, Shape shape, Vector point, Entity entity, ContourType? forceType = null)
|
||||
{
|
||||
var contourType = forceType ?? DetectContourType(shape);
|
||||
var normal = ComputeNormal(point, entity, contourType);
|
||||
var winding = DetermineWinding(shape);
|
||||
var normal = ComputeNormal(point, entity, contourType, winding);
|
||||
|
||||
var leadIn = SelectLeadIn(contourType);
|
||||
var leadOut = SelectLeadOut(contourType);
|
||||
@@ -143,29 +143,33 @@ namespace OpenNest.CNC.CuttingStrategy
|
||||
return ContourType.Internal;
|
||||
}
|
||||
|
||||
public static double ComputeNormal(Vector point, Entity entity, ContourType contourType)
|
||||
public static double ComputeNormal(Vector point, Entity entity, ContourType contourType,
|
||||
RotationType winding = RotationType.CW)
|
||||
{
|
||||
double normal;
|
||||
|
||||
if (entity is Line line)
|
||||
{
|
||||
// Perpendicular to line direction
|
||||
// Perpendicular to line direction: tangent + π/2 = left side.
|
||||
// Left side = outward for CW winding; for CCW winding, outward
|
||||
// is on the right side, so flip.
|
||||
var tangent = line.EndPoint.AngleFrom(line.StartPoint);
|
||||
normal = tangent + Math.Angle.HalfPI;
|
||||
if (winding == RotationType.CCW)
|
||||
normal += System.Math.PI;
|
||||
}
|
||||
else if (entity is Arc arc)
|
||||
{
|
||||
// Radial direction from center to point
|
||||
// Radial direction from center to point.
|
||||
// Flip when the arc direction differs from the contour winding —
|
||||
// that indicates a concave feature where radial points inward.
|
||||
normal = point.AngleFrom(arc.Center);
|
||||
|
||||
// For CCW arcs the radial points the wrong way — flip it.
|
||||
// CW arcs are convex features (corners) where radial = outward.
|
||||
// CCW arcs are concave features (slots) where radial = inward.
|
||||
if (arc.Rotation == RotationType.CCW)
|
||||
if (arc.Rotation != winding)
|
||||
normal += System.Math.PI;
|
||||
}
|
||||
else if (entity is Circle circle)
|
||||
{
|
||||
// Radial outward — always correct regardless of winding
|
||||
normal = point.AngleFrom(circle.Center);
|
||||
}
|
||||
else
|
||||
@@ -182,9 +186,10 @@ namespace OpenNest.CNC.CuttingStrategy
|
||||
|
||||
public static RotationType DetermineWinding(Shape shape)
|
||||
{
|
||||
// Use signed area: positive = CCW, negative = CW
|
||||
var area = shape.Area();
|
||||
return area >= 0 ? RotationType.CCW : RotationType.CW;
|
||||
if (shape.Entities.Count == 1 && shape.Entities[0] is Circle circle)
|
||||
return circle.Rotation;
|
||||
|
||||
return shape.ToPolygon().RotationDirection();
|
||||
}
|
||||
|
||||
private LeadIn ClampLeadInForCircle(LeadIn leadIn, Circle circle, Vector contourPoint, double normalAngle)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.CNC
|
||||
@@ -9,6 +10,8 @@ namespace OpenNest.CNC
|
||||
{
|
||||
public List<ICode> Codes;
|
||||
|
||||
public Dictionary<string, VariableDefinition> Variables { get; } = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private Mode mode;
|
||||
|
||||
public Program(Mode mode = Mode.Absolute)
|
||||
@@ -454,6 +457,9 @@ namespace OpenNest.CNC
|
||||
|
||||
pgm.Codes.AddRange(codes);
|
||||
|
||||
foreach (var kvp in Variables)
|
||||
pgm.Variables[kvp.Key] = kvp.Value;
|
||||
|
||||
return pgm;
|
||||
}
|
||||
|
||||
|
||||
+30
-14
@@ -305,6 +305,15 @@ namespace OpenNest.IO
|
||||
var writer = new StreamWriter(stream);
|
||||
writer.AutoFlush = true;
|
||||
|
||||
// Emit variable definitions before G-code
|
||||
foreach (var v in program.Variables.Values)
|
||||
{
|
||||
var line = $"{v.Name} = {v.Expression}";
|
||||
if (v.Inline) line += " inline";
|
||||
if (v.Global) line += " global";
|
||||
writer.WriteLine(line);
|
||||
}
|
||||
|
||||
writer.WriteLine(program.Mode == Mode.Absolute ? "G90" : "G91");
|
||||
|
||||
for (var i = 0; i < drawing.Program.Length; ++i)
|
||||
@@ -316,6 +325,13 @@ namespace OpenNest.IO
|
||||
stream.Position = 0;
|
||||
}
|
||||
|
||||
private string FormatCoord(double value, string axis, Dictionary<string, string> variableRefs)
|
||||
{
|
||||
if (variableRefs != null && variableRefs.TryGetValue(axis, out var varName))
|
||||
return $"${varName}";
|
||||
return System.Math.Round(value, OutputPrecision).ToString(CoordinateFormat);
|
||||
}
|
||||
|
||||
private string GetCodeString(ICode code)
|
||||
{
|
||||
switch (code.Type)
|
||||
@@ -324,16 +340,16 @@ namespace OpenNest.IO
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
var arcMove = (ArcMove)code;
|
||||
var refs = arcMove.VariableRefs;
|
||||
|
||||
var x = System.Math.Round(arcMove.EndPoint.X, OutputPrecision).ToString(CoordinateFormat);
|
||||
var y = System.Math.Round(arcMove.EndPoint.Y, OutputPrecision).ToString(CoordinateFormat);
|
||||
var i = System.Math.Round(arcMove.CenterPoint.X, OutputPrecision).ToString(CoordinateFormat);
|
||||
var j = System.Math.Round(arcMove.CenterPoint.Y, OutputPrecision).ToString(CoordinateFormat);
|
||||
var x = FormatCoord(arcMove.EndPoint.X, "X", refs);
|
||||
var y = FormatCoord(arcMove.EndPoint.Y, "Y", refs);
|
||||
var i = FormatCoord(arcMove.CenterPoint.X, "I", refs);
|
||||
var j = FormatCoord(arcMove.CenterPoint.Y, "J", refs);
|
||||
|
||||
if (arcMove.Rotation == RotationType.CW)
|
||||
sb.Append(string.Format("G02X{0}Y{1}I{2}J{3}", x, y, i, j));
|
||||
else
|
||||
sb.Append(string.Format("G03X{0}Y{1}I{2}J{3}", x, y, i, j));
|
||||
sb.Append(arcMove.Rotation == RotationType.CW
|
||||
? $"G02X{x}Y{y}I{i}J{j}"
|
||||
: $"G03X{x}Y{y}I{i}J{j}");
|
||||
|
||||
if (arcMove.Layer != LayerType.Cut)
|
||||
sb.Append(GetLayerString(arcMove.Layer));
|
||||
@@ -354,10 +370,9 @@ namespace OpenNest.IO
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
var linearMove = (LinearMove)code;
|
||||
var refs = linearMove.VariableRefs;
|
||||
|
||||
sb.Append(string.Format("G01X{0}Y{1}",
|
||||
System.Math.Round(linearMove.EndPoint.X, OutputPrecision).ToString(CoordinateFormat),
|
||||
System.Math.Round(linearMove.EndPoint.Y, OutputPrecision).ToString(CoordinateFormat)));
|
||||
sb.Append($"G01X{FormatCoord(linearMove.EndPoint.X, "X", refs)}Y{FormatCoord(linearMove.EndPoint.Y, "Y", refs)}");
|
||||
|
||||
if (linearMove.Layer != LayerType.Cut)
|
||||
sb.Append(GetLayerString(linearMove.Layer));
|
||||
@@ -371,15 +386,16 @@ namespace OpenNest.IO
|
||||
case CodeType.RapidMove:
|
||||
{
|
||||
var rapidMove = (RapidMove)code;
|
||||
var refs = rapidMove.VariableRefs;
|
||||
|
||||
return string.Format("G00X{0}Y{1}",
|
||||
System.Math.Round(rapidMove.EndPoint.X, OutputPrecision).ToString(CoordinateFormat),
|
||||
System.Math.Round(rapidMove.EndPoint.Y, OutputPrecision).ToString(CoordinateFormat));
|
||||
return $"G00X{FormatCoord(rapidMove.EndPoint.X, "X", refs)}Y{FormatCoord(rapidMove.EndPoint.Y, "Y", refs)}";
|
||||
}
|
||||
|
||||
case CodeType.SetFeedrate:
|
||||
{
|
||||
var setFeedrate = (Feedrate)code;
|
||||
if (setFeedrate.VariableRef != null)
|
||||
return $"F${setFeedrate.VariableRef}";
|
||||
return "F" + setFeedrate.Value;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace OpenNest.IO
|
||||
@@ -15,6 +19,7 @@ namespace OpenNest.IO
|
||||
private CodeSection section;
|
||||
private Program program;
|
||||
private StreamReader reader;
|
||||
private Dictionary<string, double> resolvedVariables;
|
||||
|
||||
public ProgramReader(Stream stream)
|
||||
{
|
||||
@@ -24,11 +29,38 @@ namespace OpenNest.IO
|
||||
|
||||
public Program Read()
|
||||
{
|
||||
// First pass: read all lines, collect variable definitions
|
||||
var allLines = new List<string>();
|
||||
var variableDefs = new Dictionary<string, (string expression, bool inline, bool global)>(
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
var codeLines = new List<string>();
|
||||
string line;
|
||||
|
||||
while ((line = reader.ReadLine()) != null)
|
||||
{
|
||||
block = ParseBlock(line);
|
||||
allLines.Add(line);
|
||||
if (TryParseVariableDefinition(line, out var name, out var expression, out var isInline, out var isGlobal))
|
||||
variableDefs[name] = (expression, isInline, isGlobal);
|
||||
else
|
||||
codeLines.Add(line);
|
||||
}
|
||||
|
||||
// Evaluate variables with topological sort for dependency ordering
|
||||
resolvedVariables = ResolveVariables(variableDefs);
|
||||
|
||||
// Store evaluated variables on the program
|
||||
foreach (var kvp in variableDefs)
|
||||
{
|
||||
var name = kvp.Key;
|
||||
var (expression, isInline, isGlobal) = kvp.Value;
|
||||
var value = resolvedVariables[name];
|
||||
program.Variables[name] = new VariableDefinition(name, expression, value, isInline, isGlobal);
|
||||
}
|
||||
|
||||
// Second pass: parse G-code lines with variable substitution
|
||||
foreach (var codeLine in codeLines)
|
||||
{
|
||||
block = ParseBlock(codeLine);
|
||||
ProcessCurrentBlock();
|
||||
}
|
||||
|
||||
@@ -39,10 +71,43 @@ namespace OpenNest.IO
|
||||
{
|
||||
var block = new CodeBlock();
|
||||
Code code = null;
|
||||
for (int i = 0; i < line.Length; ++i)
|
||||
for (var i = 0; i < line.Length; ++i)
|
||||
{
|
||||
var c = line[i];
|
||||
if (char.IsLetter(c))
|
||||
if (c == '$' && code != null && resolvedVariables != null)
|
||||
{
|
||||
// Read the maximal variable name (letters, digits, underscores)
|
||||
var start = i + 1;
|
||||
while (start < line.Length && (char.IsLetterOrDigit(line[start]) || line[start] == '_'))
|
||||
start++;
|
||||
var maxName = line.Substring(i + 1, start - i - 1);
|
||||
|
||||
// Try longest match first, then progressively shorter to handle
|
||||
// cases like X$widthY0 where "widthY" isn't a variable but "width" is
|
||||
string lookupKey = null;
|
||||
var nameLen = maxName.Length;
|
||||
while (nameLen > 0)
|
||||
{
|
||||
var candidate = maxName.Substring(0, nameLen);
|
||||
lookupKey = resolvedVariables.Keys
|
||||
.FirstOrDefault(k => string.Equals(k, candidate, StringComparison.OrdinalIgnoreCase));
|
||||
if (lookupKey != null)
|
||||
break;
|
||||
nameLen--;
|
||||
}
|
||||
|
||||
if (lookupKey != null)
|
||||
{
|
||||
code.Value = resolvedVariables[lookupKey].ToString(CultureInfo.InvariantCulture);
|
||||
code.VariableRef = lookupKey;
|
||||
i += nameLen; // advance past the matched variable name
|
||||
}
|
||||
else
|
||||
{
|
||||
i = start - 1; // no match, skip the whole thing
|
||||
}
|
||||
}
|
||||
else if (char.IsLetter(c))
|
||||
block.Add((code = new Code(c)));
|
||||
else if (c == ':')
|
||||
{
|
||||
@@ -125,7 +190,10 @@ namespace OpenNest.IO
|
||||
break;
|
||||
|
||||
case 'F':
|
||||
program.Codes.Add(new Feedrate() { Value = double.Parse(code.Value) });
|
||||
var feedrate = new Feedrate() { Value = double.Parse(code.Value) };
|
||||
if (code.VariableRef != null)
|
||||
feedrate.VariableRef = code.VariableRef;
|
||||
program.Codes.Add(feedrate);
|
||||
code = GetNextCode();
|
||||
break;
|
||||
|
||||
@@ -143,6 +211,7 @@ namespace OpenNest.IO
|
||||
double y = 0;
|
||||
var layer = LayerType.Cut;
|
||||
var suppressed = false;
|
||||
string xRef = null, yRef = null;
|
||||
|
||||
while (section == CodeSection.Line)
|
||||
{
|
||||
@@ -157,10 +226,12 @@ namespace OpenNest.IO
|
||||
{
|
||||
case 'X':
|
||||
x = double.Parse(code.Value);
|
||||
xRef = code.VariableRef;
|
||||
break;
|
||||
|
||||
case 'Y':
|
||||
y = double.Parse(code.Value);
|
||||
yRef = code.VariableRef;
|
||||
break;
|
||||
|
||||
case ':':
|
||||
@@ -200,10 +271,13 @@ namespace OpenNest.IO
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var refs = BuildVariableRefs(("X", xRef), ("Y", yRef));
|
||||
|
||||
if (isRapid)
|
||||
program.Codes.Add(new RapidMove(x, y));
|
||||
program.Codes.Add(new RapidMove(x, y) { VariableRefs = refs });
|
||||
else
|
||||
program.Codes.Add(new LinearMove(x, y) { Layer = layer, Suppressed = suppressed });
|
||||
program.Codes.Add(new LinearMove(x, y) { Layer = layer, Suppressed = suppressed, VariableRefs = refs });
|
||||
}
|
||||
|
||||
private void ReadArc(RotationType rotation)
|
||||
@@ -214,6 +288,7 @@ namespace OpenNest.IO
|
||||
double j = 0;
|
||||
var layer = LayerType.Cut;
|
||||
var suppressed = false;
|
||||
string xRef = null, yRef = null, iRef = null, jRef = null;
|
||||
|
||||
while (section == CodeSection.Arc)
|
||||
{
|
||||
@@ -229,18 +304,22 @@ namespace OpenNest.IO
|
||||
{
|
||||
case 'X':
|
||||
x = double.Parse(code.Value);
|
||||
xRef = code.VariableRef;
|
||||
break;
|
||||
|
||||
case 'Y':
|
||||
y = double.Parse(code.Value);
|
||||
yRef = code.VariableRef;
|
||||
break;
|
||||
|
||||
case 'I':
|
||||
i = double.Parse(code.Value);
|
||||
iRef = code.VariableRef;
|
||||
break;
|
||||
|
||||
case 'J':
|
||||
j = double.Parse(code.Value);
|
||||
jRef = code.VariableRef;
|
||||
break;
|
||||
|
||||
case ':':
|
||||
@@ -286,7 +365,8 @@ namespace OpenNest.IO
|
||||
CenterPoint = new Vector(i, j),
|
||||
Rotation = rotation,
|
||||
Layer = layer,
|
||||
Suppressed = suppressed
|
||||
Suppressed = suppressed,
|
||||
VariableRefs = BuildVariableRefs(("X", xRef), ("Y", yRef), ("I", iRef), ("J", jRef))
|
||||
});
|
||||
}
|
||||
|
||||
@@ -351,6 +431,179 @@ namespace OpenNest.IO
|
||||
return block[codeIndex];
|
||||
}
|
||||
|
||||
private static bool TryParseVariableDefinition(string line, out string name, out string expression,
|
||||
out bool isInline, out bool isGlobal)
|
||||
{
|
||||
name = null;
|
||||
expression = null;
|
||||
isInline = false;
|
||||
isGlobal = false;
|
||||
|
||||
var trimmed = line.Trim();
|
||||
if (trimmed.Length == 0)
|
||||
return false;
|
||||
|
||||
// Must start with a letter or underscore (not a G-code letter followed by a digit)
|
||||
var firstChar = trimmed[0];
|
||||
if (!char.IsLetter(firstChar) && firstChar != '_')
|
||||
return false;
|
||||
|
||||
// If line starts with a known G-code letter followed by a digit, it's not a variable
|
||||
if (trimmed.Length >= 2 && char.IsDigit(trimmed[1]))
|
||||
{
|
||||
var upper = char.ToUpper(firstChar);
|
||||
if (upper is 'G' or 'M' or 'N' or 'F' or 'X' or 'Y' or 'I' or 'J' or 'T' or 'S' or 'O' or 'P' or 'R')
|
||||
return false;
|
||||
}
|
||||
|
||||
// Must contain '='
|
||||
var eqIndex = trimmed.IndexOf('=');
|
||||
if (eqIndex < 1)
|
||||
return false;
|
||||
|
||||
// Extract name (everything before '=', trimmed)
|
||||
var rawName = trimmed.Substring(0, eqIndex).Trim();
|
||||
|
||||
// Validate name: must be identifier (letter/underscore followed by alphanumeric/underscore)
|
||||
if (rawName.Length == 0 || (!char.IsLetter(rawName[0]) && rawName[0] != '_'))
|
||||
return false;
|
||||
for (var i = 1; i < rawName.Length; i++)
|
||||
{
|
||||
if (!char.IsLetterOrDigit(rawName[i]) && rawName[i] != '_')
|
||||
return false;
|
||||
}
|
||||
|
||||
// Extract expression and flags from the remainder after '='
|
||||
var remainder = trimmed.Substring(eqIndex + 1).Trim();
|
||||
|
||||
// Check for trailing flags: inline and/or global
|
||||
// Parse from the end to separate expression from flags
|
||||
var words = remainder.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
var flagStart = words.Length;
|
||||
for (var i = words.Length - 1; i >= 0; i--)
|
||||
{
|
||||
var word = words[i].ToLowerInvariant();
|
||||
if (word == "inline" || word == "global")
|
||||
flagStart = i;
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
// Build expression from non-flag words
|
||||
var expressionParts = words.Take(flagStart).ToArray();
|
||||
if (expressionParts.Length == 0)
|
||||
return false;
|
||||
|
||||
expression = string.Join(" ", expressionParts);
|
||||
|
||||
// Parse flags
|
||||
for (var i = flagStart; i < words.Length; i++)
|
||||
{
|
||||
var word = words[i].ToLowerInvariant();
|
||||
if (word == "inline") isInline = true;
|
||||
else if (word == "global") isGlobal = true;
|
||||
}
|
||||
|
||||
name = rawName;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static Dictionary<string, double> ResolveVariables(
|
||||
Dictionary<string, (string expression, bool inline, bool global)> variableDefs)
|
||||
{
|
||||
if (variableDefs.Count == 0)
|
||||
return new Dictionary<string, double>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// Build dependency graph
|
||||
var dependencies = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var kvp in variableDefs)
|
||||
{
|
||||
var deps = new List<string>();
|
||||
var expr = kvp.Value.expression;
|
||||
for (var i = 0; i < expr.Length; i++)
|
||||
{
|
||||
if (expr[i] == '$')
|
||||
{
|
||||
var start = i + 1;
|
||||
while (start < expr.Length && (char.IsLetterOrDigit(expr[start]) || expr[start] == '_'))
|
||||
start++;
|
||||
var refName = expr.Substring(i + 1, start - i - 1);
|
||||
// Find the canonical name (case-insensitive match)
|
||||
var canonical = variableDefs.Keys
|
||||
.FirstOrDefault(k => string.Equals(k, refName, StringComparison.OrdinalIgnoreCase));
|
||||
if (canonical != null)
|
||||
deps.Add(canonical);
|
||||
i = start - 1;
|
||||
}
|
||||
}
|
||||
dependencies[kvp.Key] = deps;
|
||||
}
|
||||
|
||||
// Topological sort (Kahn's algorithm)
|
||||
var inDegree = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var name in variableDefs.Keys)
|
||||
inDegree[name] = 0;
|
||||
foreach (var kvp in dependencies)
|
||||
{
|
||||
foreach (var dep in kvp.Value)
|
||||
{
|
||||
if (inDegree.ContainsKey(dep))
|
||||
inDegree[kvp.Key]++;
|
||||
}
|
||||
}
|
||||
|
||||
var queue = new Queue<string>();
|
||||
foreach (var kvp in inDegree)
|
||||
{
|
||||
if (kvp.Value == 0)
|
||||
queue.Enqueue(kvp.Key);
|
||||
}
|
||||
|
||||
var resolved = new Dictionary<string, double>(StringComparer.OrdinalIgnoreCase);
|
||||
var order = new List<string>();
|
||||
|
||||
while (queue.Count > 0)
|
||||
{
|
||||
var current = queue.Dequeue();
|
||||
order.Add(current);
|
||||
|
||||
// Evaluate this variable
|
||||
var expr = variableDefs[current].expression;
|
||||
var value = ExpressionEvaluator.Evaluate(expr, resolved);
|
||||
resolved[current] = value;
|
||||
|
||||
// Reduce in-degree of dependents
|
||||
foreach (var kvp in dependencies)
|
||||
{
|
||||
if (kvp.Value.Contains(current, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
inDegree[kvp.Key]--;
|
||||
if (inDegree[kvp.Key] == 0 && !order.Contains(kvp.Key))
|
||||
queue.Enqueue(kvp.Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (order.Count != variableDefs.Count)
|
||||
throw new InvalidOperationException("Circular dependency detected among variables.");
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> BuildVariableRefs(params (string axis, string varRef)[] refs)
|
||||
{
|
||||
Dictionary<string, string> result = null;
|
||||
foreach (var (axis, varRef) in refs)
|
||||
{
|
||||
if (varRef != null)
|
||||
{
|
||||
result ??= new Dictionary<string, string>();
|
||||
result[axis] = varRef;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
reader.Close();
|
||||
@@ -374,6 +627,8 @@ namespace OpenNest.IO
|
||||
|
||||
public string Value { get; set; }
|
||||
|
||||
public string VariableRef { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Id + Value;
|
||||
|
||||
@@ -29,6 +29,29 @@ 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>
|
||||
/// 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>
|
||||
@@ -63,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))
|
||||
@@ -112,7 +135,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 +163,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 +204,52 @@ 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).
|
||||
/// 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
|
||||
&& ctx.UserVariableMapping.TryGetValue((ctx.DrawingId, varName), out var varNum))
|
||||
{
|
||||
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)
|
||||
@@ -195,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());
|
||||
}
|
||||
|
||||
@@ -66,7 +66,6 @@ public sealed class CincinnatiPartSubprogramWriter
|
||||
_featureWriter.Write(w, ctx);
|
||||
}
|
||||
|
||||
w.WriteLine("G0 X0 Y0");
|
||||
w.WriteLine($"M99 (END OF {drawingName})");
|
||||
}
|
||||
|
||||
|
||||
@@ -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,42 +82,41 @@ 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
|
||||
preamble.WriteMainProgram(writer, nest.Name ?? "NEST", materialDesc, plates.Count, initialCutLibrary);
|
||||
preamble.WriteMainProgram(writer, nest.Name ?? "NEST", materialDesc, plates, initialCutLibrary);
|
||||
|
||||
// Variable declaration subprogram
|
||||
preamble.WriteVariableDeclaration(writer, vars);
|
||||
|
||||
// Sheet subprograms
|
||||
// Sheet subprograms (one per unique layout, quantity handled via L count in main)
|
||||
for (var i = 0; i < plates.Count; i++)
|
||||
{
|
||||
var plate = plates[i];
|
||||
var sheetIndex = i + 1;
|
||||
var layoutIndex = i + 1;
|
||||
var subNumber = Config.SheetSubprogramStart + i;
|
||||
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);
|
||||
sheetWriter.Write(writer, plate, nest.Name ?? "NEST", layoutIndex, subNumber,
|
||||
cutLibrary, etchLibrary, partSubprograms, userVarMapping);
|
||||
}
|
||||
|
||||
// Part sub-programs (if enabled)
|
||||
@@ -142,6 +144,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();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using OpenNest;
|
||||
using OpenNest.CNC;
|
||||
@@ -23,7 +24,7 @@ public sealed class CincinnatiPreambleWriter
|
||||
/// </summary>
|
||||
/// <param name="initialLibrary">Resolved G89 library file for the initial process setup.</param>
|
||||
public void WriteMainProgram(TextWriter w, string nestName, string materialDescription,
|
||||
int sheetCount, string initialLibrary)
|
||||
List<Plate> plates, string initialLibrary)
|
||||
{
|
||||
w.WriteLine(CoordinateFormatter.Comment($"NEST {nestName}"));
|
||||
w.WriteLine(CoordinateFormatter.Comment($"CONFIGURATION - {_config.ConfigurationName}"));
|
||||
@@ -54,10 +55,16 @@ public sealed class CincinnatiPreambleWriter
|
||||
|
||||
w.WriteLine("GOTO1 (GOTO SHEET NUMBER)");
|
||||
|
||||
for (var i = 1; i <= sheetCount; i++)
|
||||
for (var i = 0; i < plates.Count; i++)
|
||||
{
|
||||
var subNum = _config.SheetSubprogramStart + (i - 1);
|
||||
w.WriteLine($"N{i} M98 P{subNum} (SHEET {i})");
|
||||
var layoutNumber = i + 1;
|
||||
var subNum = _config.SheetSubprogramStart + i;
|
||||
var qty = System.Math.Max(plates[i].Quantity, 1);
|
||||
var lParam = qty > 1 ? $" L{qty}" : "";
|
||||
var sheetLabel = qty > 1
|
||||
? $"LAYOUT {layoutNumber} - {qty} SHEETS"
|
||||
: $"LAYOUT {layoutNumber}";
|
||||
w.WriteLine($"N{layoutNumber} M98 P{subNum}{lParam} ({sheetLabel})");
|
||||
}
|
||||
|
||||
w.WriteLine("M42");
|
||||
|
||||
@@ -35,10 +35,10 @@ public sealed class CincinnatiSheetWriter
|
||||
/// Optional mapping of (drawingId, rotationKey) to sub-program number.
|
||||
/// When provided, non-cutoff parts are emitted as M98 calls instead of inline features.
|
||||
/// </param>
|
||||
public void Write(TextWriter w, Plate plate, string nestName, int sheetIndex, int subNumber,
|
||||
public void Write(TextWriter w, Plate plate, string nestName, int layoutIndex, int subNumber,
|
||||
string cutLibrary, string etchLibrary,
|
||||
Dictionary<(int, long), int> partSubprograms = null,
|
||||
bool isLastSheet = false)
|
||||
Dictionary<(int drawingId, string varName), int> userVarMapping = null)
|
||||
{
|
||||
if (plate.Parts.Count == 0)
|
||||
return;
|
||||
@@ -51,11 +51,10 @@ public sealed class CincinnatiSheetWriter
|
||||
|
||||
// 1. Sheet header
|
||||
w.WriteLine("(*****************************************************)");
|
||||
w.WriteLine($"( START OF {nestName}.{sheetIndex:D3} )");
|
||||
w.WriteLine($"( START OF {nestName}.{layoutIndex:D3} )");
|
||||
w.WriteLine($":{subNumber}");
|
||||
w.WriteLine($"( Sheet {sheetIndex} )");
|
||||
w.WriteLine($"( Layout {sheetIndex} )");
|
||||
w.WriteLine($"( SHEET NAME = {_fmt.FormatCoord(length)} X {_fmt.FormatCoord(width)} )");
|
||||
w.WriteLine($"( Layout {layoutIndex} )");
|
||||
w.WriteLine($"( SHEET NAME = {_fmt.FormatCoord(width)} X {_fmt.FormatCoord(length)} )");
|
||||
w.WriteLine($"( Total parts on sheet = {partCount} )");
|
||||
w.WriteLine($"#{_config.SheetWidthVariable}={_fmt.FormatCoord(width)} (SHEET WIDTH FOR CUTOFFS)");
|
||||
w.WriteLine($"#{_config.SheetLengthVariable}={_fmt.FormatCoord(length)} (SHEET LENGTH FOR CUTOFFS)");
|
||||
@@ -88,23 +87,22 @@ public sealed class CincinnatiSheetWriter
|
||||
|
||||
// 4. Emit parts
|
||||
if (partSubprograms != null)
|
||||
WritePartsWithSubprograms(w, allParts, cutLibrary, etchLibrary, sheetDiagonal, partSubprograms);
|
||||
WritePartsWithSubprograms(w, allParts, cutLibrary, etchLibrary, sheetDiagonal, width, length, partSubprograms, userVarMapping);
|
||||
else
|
||||
WritePartsInline(w, allParts, cutLibrary, etchLibrary, sheetDiagonal);
|
||||
WritePartsInline(w, allParts, cutLibrary, etchLibrary, sheetDiagonal, width, length, userVarMapping);
|
||||
|
||||
// 5. Footer
|
||||
w.WriteLine("M42");
|
||||
w.WriteLine("G0 X0 Y0");
|
||||
var emitM50 = _config.PalletExchange == PalletMode.EndOfSheet
|
||||
|| (_config.PalletExchange == PalletMode.StartAndEnd && isLastSheet);
|
||||
if (emitM50)
|
||||
w.WriteLine($"N{sheetIndex + 1} M50");
|
||||
w.WriteLine($"M99 (END OF {nestName}.{sheetIndex:D3})");
|
||||
if (_config.PalletExchange != PalletMode.None)
|
||||
w.WriteLine("M50");
|
||||
w.WriteLine($"M99 (END OF {nestName}.{layoutIndex:D3})");
|
||||
}
|
||||
|
||||
private void WritePartsWithSubprograms(TextWriter w, List<Part> allParts,
|
||||
string cutLibrary, string etchLibrary, double sheetDiagonal,
|
||||
Dictionary<(int, long), int> partSubprograms)
|
||||
double plateWidth, double plateLength,
|
||||
Dictionary<(int, long), int> partSubprograms,
|
||||
Dictionary<(int drawingId, string varName), int> userVarMapping)
|
||||
{
|
||||
var lastPartName = "";
|
||||
var featureIndex = 0;
|
||||
@@ -154,7 +152,12 @@ public sealed class CincinnatiSheetWriter
|
||||
LibraryFile = isEtch ? etchLibrary : cutLibrary,
|
||||
CutDistance = cutDistance,
|
||||
SheetDiagonal = sheetDiagonal,
|
||||
PartLocation = part.Location
|
||||
PartLocation = part.Location,
|
||||
UserVariableMapping = userVarMapping,
|
||||
DrawingId = part.BaseDrawing.Id,
|
||||
IsCutOff = part.BaseDrawing.IsCutOff,
|
||||
PlateWidth = plateWidth,
|
||||
PlateLength = plateLength
|
||||
};
|
||||
|
||||
_featureWriter.Write(w, ctx);
|
||||
@@ -202,7 +205,9 @@ public sealed class CincinnatiSheetWriter
|
||||
}
|
||||
|
||||
private void WritePartsInline(TextWriter w, List<Part> allParts,
|
||||
string cutLibrary, string etchLibrary, double sheetDiagonal)
|
||||
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
|
||||
var features = new List<(Part part, List<ICode> codes, bool isEtch)>();
|
||||
@@ -242,7 +247,12 @@ public sealed class CincinnatiSheetWriter
|
||||
LibraryFile = isEtch ? etchLibrary : cutLibrary,
|
||||
CutDistance = cutDistance,
|
||||
SheetDiagonal = sheetDiagonal,
|
||||
PartLocation = part.Location
|
||||
PartLocation = part.Location,
|
||||
UserVariableMapping = userVarMapping,
|
||||
DrawingId = part.BaseDrawing.Id,
|
||||
IsCutOff = part.BaseDrawing.IsCutOff,
|
||||
PlateWidth = plateWidth,
|
||||
PlateLength = plateLength
|
||||
};
|
||||
|
||||
_featureWriter.Write(w, ctx);
|
||||
|
||||
@@ -101,4 +101,35 @@ public class ProgramVariableTests
|
||||
move.Offset(5.0, 0);
|
||||
Assert.Null(move.VariableRefs);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Program_Variables_EmptyByDefault()
|
||||
{
|
||||
var pgm = new Program();
|
||||
Assert.Empty(pgm.Variables);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Program_Variables_CaseInsensitive()
|
||||
{
|
||||
var pgm = new Program();
|
||||
pgm.Variables["Diameter"] = new VariableDefinition("Diameter", "0.3", 0.3);
|
||||
Assert.True(pgm.Variables.ContainsKey("diameter"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Program_Clone_DeepCopiesVariables()
|
||||
{
|
||||
var pgm = new Program();
|
||||
pgm.Variables["diameter"] = new VariableDefinition("diameter", "0.3", 0.3);
|
||||
pgm.Codes.Add(new LinearMove(1.0, 0));
|
||||
|
||||
var clone = (Program)pgm.Clone();
|
||||
|
||||
Assert.Single(clone.Variables);
|
||||
Assert.Equal(0.3, clone.Variables["diameter"].Value);
|
||||
// Verify it's a separate dictionary
|
||||
clone.Variables.Remove("diameter");
|
||||
Assert.Single(pgm.Variables);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ public class CincinnatiPostProcessorTests
|
||||
|
||||
// Sheet subprogram
|
||||
Assert.Contains(":101", output);
|
||||
Assert.Contains("( Sheet 1 )", output);
|
||||
Assert.Contains("( Layout 1 )", output);
|
||||
Assert.Contains("G84", output);
|
||||
Assert.Contains("M99", output);
|
||||
}
|
||||
@@ -150,8 +150,8 @@ public class CincinnatiPostProcessorTests
|
||||
var output = Encoding.UTF8.GetString(ms.ToArray());
|
||||
|
||||
// Should only have one sheet subprogram call in main
|
||||
Assert.Contains("N1 M98 P101 (SHEET 1)", output);
|
||||
Assert.DoesNotContain("SHEET 2", output);
|
||||
Assert.Contains("N1 M98 P101 (LAYOUT 1)", output);
|
||||
Assert.DoesNotContain("LAYOUT 2", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -258,8 +258,7 @@ public class CincinnatiPostProcessorTests
|
||||
Assert.Contains(":200", output);
|
||||
Assert.Contains("G84", output);
|
||||
|
||||
// Sub-program ends with G0 X0 Y0 and M99
|
||||
Assert.Contains("G0 X0 Y0", output);
|
||||
// Sub-program ends with M99
|
||||
Assert.Contains("M99 (END OF Square)", output);
|
||||
|
||||
// G92 restore after M98 call
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using OpenNest.CNC;
|
||||
@@ -19,7 +20,8 @@ public class CincinnatiPreambleWriterTests
|
||||
using var sw = new StringWriter(sb);
|
||||
var writer = new CincinnatiPreambleWriter(config);
|
||||
|
||||
writer.WriteMainProgram(sw, "TestNest", "Mild Steel, 10GA", 2, "MS135N2PANEL.lib");
|
||||
var plates = new List<Plate> { new(48, 96), new(48, 96) };
|
||||
writer.WriteMainProgram(sw, "TestNest", "Mild Steel, 10GA", plates, "MS135N2PANEL.lib");
|
||||
|
||||
var output = sb.ToString();
|
||||
Assert.Contains("( NEST TestNest )", output);
|
||||
@@ -29,8 +31,8 @@ public class CincinnatiPreambleWriterTests
|
||||
Assert.Contains("G89 PMS135N2PANEL.lib", output);
|
||||
Assert.Contains("M98 P100 (Variable Declaration)", output);
|
||||
Assert.Contains("GOTO1 (GOTO SHEET NUMBER)", output);
|
||||
Assert.Contains("N1 M98 P101 (SHEET 1)", output);
|
||||
Assert.Contains("N2 M98 P102 (SHEET 2)", output);
|
||||
Assert.Contains("N1 M98 P101 (LAYOUT 1)", output);
|
||||
Assert.Contains("N2 M98 P102 (LAYOUT 2)", output);
|
||||
Assert.Contains("M30 (END OF MAIN)", output);
|
||||
}
|
||||
|
||||
@@ -42,7 +44,7 @@ public class CincinnatiPreambleWriterTests
|
||||
using var sw = new StringWriter(sb);
|
||||
var writer = new CincinnatiPreambleWriter(config);
|
||||
|
||||
writer.WriteMainProgram(sw, "Test", "", 1, "");
|
||||
writer.WriteMainProgram(sw, "Test", "", new List<Plate> { new(48, 96) }, "");
|
||||
|
||||
Assert.Contains("G21 G90", sb.ToString());
|
||||
}
|
||||
@@ -55,7 +57,7 @@ public class CincinnatiPreambleWriterTests
|
||||
using var sw = new StringWriter(sb);
|
||||
var writer = new CincinnatiPreambleWriter(config);
|
||||
|
||||
writer.WriteMainProgram(sw, "Test", "", 1, "");
|
||||
writer.WriteMainProgram(sw, "Test", "", new List<Plate> { new(48, 96) }, "");
|
||||
|
||||
Assert.Contains("G20 G90", sb.ToString());
|
||||
}
|
||||
@@ -68,7 +70,7 @@ public class CincinnatiPreambleWriterTests
|
||||
using var sw = new StringWriter(sb);
|
||||
var writer = new CincinnatiPreambleWriter(config);
|
||||
|
||||
writer.WriteMainProgram(sw, "Test", "", 1, "");
|
||||
writer.WriteMainProgram(sw, "Test", "", new List<Plate> { new(48, 96) }, "");
|
||||
|
||||
Assert.Contains("G121 (SMART RAPIDS)", sb.ToString());
|
||||
}
|
||||
@@ -81,7 +83,7 @@ public class CincinnatiPreambleWriterTests
|
||||
using var sw = new StringWriter(sb);
|
||||
var writer = new CincinnatiPreambleWriter(config);
|
||||
|
||||
writer.WriteMainProgram(sw, "Test", "", 1, "");
|
||||
writer.WriteMainProgram(sw, "Test", "", new List<Plate> { new(48, 96) }, "");
|
||||
|
||||
Assert.DoesNotContain("G121", sb.ToString());
|
||||
}
|
||||
@@ -94,7 +96,7 @@ public class CincinnatiPreambleWriterTests
|
||||
using var sw = new StringWriter(sb);
|
||||
var writer = new CincinnatiPreambleWriter(config);
|
||||
|
||||
writer.WriteMainProgram(sw, "Test", "", 1, "");
|
||||
writer.WriteMainProgram(sw, "Test", "", new List<Plate> { new(48, 96) }, "");
|
||||
|
||||
Assert.Contains("M50", sb.ToString());
|
||||
}
|
||||
@@ -107,7 +109,7 @@ public class CincinnatiPreambleWriterTests
|
||||
using var sw = new StringWriter(sb);
|
||||
var writer = new CincinnatiPreambleWriter(config);
|
||||
|
||||
writer.WriteMainProgram(sw, "Test", "", 1, "");
|
||||
writer.WriteMainProgram(sw, "Test", "", new List<Plate> { new(48, 96) }, "");
|
||||
|
||||
Assert.DoesNotContain("M50", sb.ToString());
|
||||
}
|
||||
@@ -120,7 +122,7 @@ public class CincinnatiPreambleWriterTests
|
||||
using var sw = new StringWriter(sb);
|
||||
var writer = new CincinnatiPreambleWriter(config);
|
||||
|
||||
writer.WriteMainProgram(sw, "Test", "", 1, "");
|
||||
writer.WriteMainProgram(sw, "Test", "", new List<Plate> { new(48, 96) }, "");
|
||||
|
||||
Assert.Contains("G61", sb.ToString());
|
||||
}
|
||||
@@ -133,11 +135,33 @@ public class CincinnatiPreambleWriterTests
|
||||
using var sw = new StringWriter(sb);
|
||||
var writer = new CincinnatiPreambleWriter(config);
|
||||
|
||||
writer.WriteMainProgram(sw, "Test", "", 1, "");
|
||||
writer.WriteMainProgram(sw, "Test", "", new List<Plate> { new(48, 96) }, "");
|
||||
|
||||
Assert.DoesNotContain("G61", sb.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteMainProgram_EmitsLCount_WhenQuantityGreaterThanOne()
|
||||
{
|
||||
var config = new CincinnatiPostConfig { PostedUnits = Units.Inches };
|
||||
var sb = new StringBuilder();
|
||||
using var sw = new StringWriter(sb);
|
||||
var writer = new CincinnatiPreambleWriter(config);
|
||||
|
||||
var plates = new List<Plate>
|
||||
{
|
||||
new(48, 96) { Quantity = 5 },
|
||||
new(72, 48) { Quantity = 2 },
|
||||
new(36, 48) { Quantity = 1 }
|
||||
};
|
||||
writer.WriteMainProgram(sw, "Test", "", plates, "");
|
||||
|
||||
var output = sb.ToString();
|
||||
Assert.Contains("N1 M98 P101 L5 (LAYOUT 1 - 5 SHEETS)", output);
|
||||
Assert.Contains("N2 M98 P102 L2 (LAYOUT 2 - 2 SHEETS)", output);
|
||||
Assert.Contains("N3 M98 P103 (LAYOUT 3)", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteVariableDeclaration_EmitsSubprogram()
|
||||
{
|
||||
|
||||
@@ -28,7 +28,7 @@ public class CincinnatiSheetWriterTests
|
||||
|
||||
var output = sb.ToString();
|
||||
Assert.Contains(":101", output);
|
||||
Assert.Contains("( Sheet 1 )", output);
|
||||
Assert.Contains("( Layout 1 )", output);
|
||||
Assert.Contains("#110=", output);
|
||||
Assert.Contains("#111=", output);
|
||||
Assert.Contains("G92 X#5021 Y#5022", output);
|
||||
@@ -55,7 +55,6 @@ public class CincinnatiSheetWriterTests
|
||||
|
||||
var output = sb.ToString();
|
||||
Assert.Contains("M42", output);
|
||||
Assert.Contains("G0 X0 Y0", output);
|
||||
Assert.Contains("M50", output);
|
||||
}
|
||||
|
||||
@@ -143,7 +142,7 @@ public class CincinnatiSheetWriterTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteSheet_StartAndEnd_NoM50OnNonLastSheet()
|
||||
public void WriteSheet_StartAndEnd_EmitsM50()
|
||||
{
|
||||
var config = new CincinnatiPostConfig
|
||||
{
|
||||
@@ -157,33 +156,33 @@ public class CincinnatiSheetWriterTests
|
||||
using var sw = new StringWriter(sb);
|
||||
var sheetWriter = new CincinnatiSheetWriter(config, new ProgramVariableManager());
|
||||
|
||||
sheetWriter.Write(sw, plate, "TestNest", 1, 101, "", "", isLastSheet: false);
|
||||
|
||||
var output = sb.ToString();
|
||||
Assert.DoesNotContain("M50", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteSheet_StartAndEnd_M50OnLastSheet()
|
||||
{
|
||||
var config = new CincinnatiPostConfig
|
||||
{
|
||||
PalletExchange = PalletMode.StartAndEnd,
|
||||
PostedAccuracy = 4
|
||||
};
|
||||
var plate = new Plate(48.0, 96.0);
|
||||
plate.Parts.Add(new Part(new Drawing("TestPart", CreateSimpleProgram())));
|
||||
|
||||
var sb = new StringBuilder();
|
||||
using var sw = new StringWriter(sb);
|
||||
var sheetWriter = new CincinnatiSheetWriter(config, new ProgramVariableManager());
|
||||
|
||||
sheetWriter.Write(sw, plate, "TestNest", 1, 101, "", "", isLastSheet: true);
|
||||
sheetWriter.Write(sw, plate, "TestNest", 1, 101, "", "");
|
||||
|
||||
var output = sb.ToString();
|
||||
Assert.Contains("M50", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteSheet_NoPalletExchange_OmitsM50()
|
||||
{
|
||||
var config = new CincinnatiPostConfig
|
||||
{
|
||||
PalletExchange = PalletMode.None,
|
||||
PostedAccuracy = 4
|
||||
};
|
||||
var plate = new Plate(48.0, 96.0);
|
||||
plate.Parts.Add(new Part(new Drawing("TestPart", CreateSimpleProgram())));
|
||||
|
||||
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();
|
||||
Assert.DoesNotContain("M50", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteSheet_EndOfSheet_AlwaysEmitsM50()
|
||||
{
|
||||
@@ -199,7 +198,7 @@ public class CincinnatiSheetWriterTests
|
||||
using var sw = new StringWriter(sb);
|
||||
var sheetWriter = new CincinnatiSheetWriter(config, new ProgramVariableManager());
|
||||
|
||||
sheetWriter.Write(sw, plate, "TestNest", 1, 101, "", "", isLastSheet: false);
|
||||
sheetWriter.Write(sw, plate, "TestNest", 1, 101, "", "");
|
||||
|
||||
var output = sb.ToString();
|
||||
Assert.Contains("M50", output);
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
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);
|
||||
}
|
||||
|
||||
[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);
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.IO;
|
||||
|
||||
namespace OpenNest.Tests.IO;
|
||||
|
||||
public class NestWriterVariableTests
|
||||
{
|
||||
[Fact]
|
||||
public void RoundTrip_VariableDefinitions_Preserved()
|
||||
{
|
||||
var nest = CreateNestWithVariableProgram(
|
||||
"width = 48.0 global\ndiameter = 0.3\nG90\nG01X$widthY$diameter");
|
||||
|
||||
var loaded = RoundTrip(nest);
|
||||
var pgm = loaded.Drawings.First().Program;
|
||||
|
||||
Assert.Equal(2, pgm.Variables.Count);
|
||||
Assert.Equal(48.0, pgm.Variables["width"].Value);
|
||||
Assert.True(pgm.Variables["width"].Global);
|
||||
Assert.Equal(0.3, pgm.Variables["diameter"].Value);
|
||||
Assert.False(pgm.Variables["diameter"].Global);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RoundTrip_VariableRefs_Preserved()
|
||||
{
|
||||
var nest = CreateNestWithVariableProgram(
|
||||
"width = 48.0\nG90\nG01X$widthY0");
|
||||
|
||||
var loaded = RoundTrip(nest);
|
||||
var pgm = loaded.Drawings.First().Program;
|
||||
|
||||
var linear = (LinearMove)pgm.Codes[0];
|
||||
Assert.Equal(48.0, linear.EndPoint.X);
|
||||
Assert.NotNull(linear.VariableRefs);
|
||||
Assert.Equal("width", linear.VariableRefs["X"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RoundTrip_InlineFlag_Preserved()
|
||||
{
|
||||
var nest = CreateNestWithVariableProgram(
|
||||
"kerf = 0.06 inline\nG90\nG01X1Y0");
|
||||
|
||||
var loaded = RoundTrip(nest);
|
||||
var pgm = loaded.Drawings.First().Program;
|
||||
|
||||
Assert.True(pgm.Variables["kerf"].Inline);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RoundTrip_NoVariables_WorksAsNormal()
|
||||
{
|
||||
var nest = CreateNestWithVariableProgram("G90\nG01X1Y2");
|
||||
|
||||
var loaded = RoundTrip(nest);
|
||||
var pgm = loaded.Drawings.First().Program;
|
||||
|
||||
Assert.Empty(pgm.Variables);
|
||||
var linear = (LinearMove)pgm.Codes[0];
|
||||
Assert.Equal(1.0, linear.EndPoint.X);
|
||||
}
|
||||
|
||||
private static Nest CreateNestWithVariableProgram(string gcode)
|
||||
{
|
||||
var stream = new MemoryStream(Encoding.UTF8.GetBytes(gcode));
|
||||
var reader = new ProgramReader(stream);
|
||||
var program = reader.Read();
|
||||
reader.Close();
|
||||
|
||||
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);
|
||||
return nest;
|
||||
}
|
||||
|
||||
private static Nest RoundTrip(Nest nest)
|
||||
{
|
||||
var ms = new MemoryStream();
|
||||
new NestWriter(nest).Write(ms);
|
||||
ms.Position = 0;
|
||||
return new NestReader(ms).Read();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.IO;
|
||||
|
||||
namespace OpenNest.Tests.IO;
|
||||
|
||||
public class ProgramReaderVariableTests
|
||||
{
|
||||
private Program Parse(string gcode)
|
||||
{
|
||||
var stream = new MemoryStream(Encoding.UTF8.GetBytes(gcode));
|
||||
var reader = new ProgramReader(stream);
|
||||
var program = reader.Read();
|
||||
reader.Close();
|
||||
return program;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_SimpleVariable_StoredInVariables()
|
||||
{
|
||||
var pgm = Parse("diameter = 0.3\nG90\nG01X1Y0");
|
||||
Assert.True(pgm.Variables.ContainsKey("diameter"));
|
||||
Assert.Equal(0.3, pgm.Variables["diameter"].Value);
|
||||
Assert.Equal("0.3", pgm.Variables["diameter"].Expression);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_VariableWithInlineFlag()
|
||||
{
|
||||
var pgm = Parse("kerf = 0.06 inline\nG90\nG01X1Y0");
|
||||
Assert.True(pgm.Variables["kerf"].Inline);
|
||||
Assert.False(pgm.Variables["kerf"].Global);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_VariableWithGlobalFlag()
|
||||
{
|
||||
var pgm = Parse("sheet_width = 48.0 global\nG90\nG01X1Y0");
|
||||
Assert.True(pgm.Variables["sheet_width"].Global);
|
||||
Assert.False(pgm.Variables["sheet_width"].Inline);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_VariableWithBothFlags()
|
||||
{
|
||||
var pgm = Parse("speed = 200 global inline\nG90\nG01X1Y0");
|
||||
Assert.True(pgm.Variables["speed"].Global);
|
||||
Assert.True(pgm.Variables["speed"].Inline);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_VariableReference_SubstitutedInCoordinate()
|
||||
{
|
||||
var pgm = Parse("width = 48.0\nG90\nG01X$widthY0");
|
||||
var linear = (LinearMove)pgm.Codes[0];
|
||||
Assert.Equal(48.0, linear.EndPoint.X);
|
||||
Assert.Equal(0.0, linear.EndPoint.Y);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_VariableReference_TrackedInVariableRefs()
|
||||
{
|
||||
var pgm = Parse("width = 48.0\nG90\nG01X$widthY0");
|
||||
var linear = (LinearMove)pgm.Codes[0];
|
||||
Assert.NotNull(linear.VariableRefs);
|
||||
Assert.Equal("width", linear.VariableRefs["X"]);
|
||||
Assert.False(linear.VariableRefs.ContainsKey("Y"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_VariableExpression_WithReference()
|
||||
{
|
||||
var pgm = Parse("diameter = 0.6\nradius = $diameter / 2\nG90\nG02X1Y0I$radiusJ0");
|
||||
Assert.Equal(0.3, pgm.Variables["radius"].Value, 10);
|
||||
var arc = (ArcMove)pgm.Codes[0];
|
||||
Assert.Equal(0.3, arc.CenterPoint.X, 10);
|
||||
Assert.Equal("radius", arc.VariableRefs["I"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_FeedVariable_TrackedOnFeedrate()
|
||||
{
|
||||
var pgm = Parse("speed = 100\nG90\nF$speed\nG01X1Y0");
|
||||
var feedrate = (Feedrate)pgm.Codes[0];
|
||||
Assert.Equal(100.0, feedrate.Value);
|
||||
Assert.Equal("speed", feedrate.VariableRef);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_VariablesCollectedInPrepass_OrderIndependent()
|
||||
{
|
||||
var pgm = Parse("radius = $diameter / 2\ndiameter = 0.6\nG90\nG01X$radiusY0");
|
||||
Assert.Equal(0.3, pgm.Variables["radius"].Value, 10);
|
||||
var linear = (LinearMove)pgm.Codes[0];
|
||||
Assert.Equal(0.3, linear.EndPoint.X, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_NoVariables_WorksAsNormal()
|
||||
{
|
||||
var pgm = Parse("G90\nG01X1.5Y2.5");
|
||||
Assert.Empty(pgm.Variables);
|
||||
var linear = (LinearMove)pgm.Codes[0];
|
||||
Assert.Equal(1.5, linear.EndPoint.X);
|
||||
Assert.Null(linear.VariableRefs);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_RapidMove_WithVariableRef()
|
||||
{
|
||||
var pgm = Parse("start_x = 5.0\nG90\nG00X$start_xY0");
|
||||
var rapid = (RapidMove)pgm.Codes[0];
|
||||
Assert.Equal(5.0, rapid.EndPoint.X);
|
||||
Assert.Equal("start_x", rapid.VariableRefs["X"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_ArcMove_VariableOnMultipleAxes()
|
||||
{
|
||||
var pgm = Parse("r = 0.5\nG90\nG03X1Y0I$rJ$r");
|
||||
var arc = (ArcMove)pgm.Codes[0];
|
||||
Assert.Equal(0.5, arc.CenterPoint.X);
|
||||
Assert.Equal(0.5, arc.CenterPoint.Y);
|
||||
Assert.Equal("r", arc.VariableRefs["I"]);
|
||||
Assert.Equal("r", arc.VariableRefs["J"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_CaseInsensitive_VariableReference()
|
||||
{
|
||||
var pgm = Parse("Diameter = 0.3\nG90\nG01X$diameterY0");
|
||||
var linear = (LinearMove)pgm.Codes[0];
|
||||
Assert.Equal(0.3, linear.EndPoint.X);
|
||||
}
|
||||
}
|
||||
@@ -102,7 +102,7 @@ namespace OpenNest.Actions
|
||||
snapPoint = closest;
|
||||
snapEntity = entity;
|
||||
snapContourType = info.ContourType;
|
||||
snapNormal = ContourCuttingStrategy.ComputeNormal(closest, entity, info.ContourType);
|
||||
snapNormal = ContourCuttingStrategy.ComputeNormal(closest, entity, info.ContourType, info.Winding);
|
||||
hasSnap = true;
|
||||
hoveredContour = info;
|
||||
}
|
||||
@@ -282,7 +282,7 @@ namespace OpenNest.Actions
|
||||
{
|
||||
snapPoint = bestPoint;
|
||||
snapEntity = bestEntity;
|
||||
snapNormal = ContourCuttingStrategy.ComputeNormal(bestPoint, bestEntity, snapContourType);
|
||||
snapNormal = ContourCuttingStrategy.ComputeNormal(bestPoint, bestEntity, snapContourType, hoveredContour.Winding);
|
||||
activeSnapType = bestType;
|
||||
}
|
||||
|
||||
@@ -356,7 +356,8 @@ namespace OpenNest.Actions
|
||||
contours.Add(new ShapeInfo
|
||||
{
|
||||
Shape = profile.Perimeter,
|
||||
ContourType = ContourType.External
|
||||
ContourType = ContourType.External,
|
||||
Winding = ContourCuttingStrategy.DetermineWinding(profile.Perimeter)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -366,7 +367,8 @@ namespace OpenNest.Actions
|
||||
contours.Add(new ShapeInfo
|
||||
{
|
||||
Shape = cutout,
|
||||
ContourType = ContourCuttingStrategy.DetectContourType(cutout)
|
||||
ContourType = ContourCuttingStrategy.DetectContourType(cutout),
|
||||
Winding = ContourCuttingStrategy.DetermineWinding(cutout)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -483,6 +485,7 @@ namespace OpenNest.Actions
|
||||
{
|
||||
public Shape Shape { get; set; }
|
||||
public ContourType ContourType { get; set; }
|
||||
public RotationType Winding { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ OpenNest takes your part drawings, lets you define your sheet (plate) sizes, and
|
||||
- **Lead-In/Lead-Out & Tabs** — Configurable approach paths, exit paths, and holding tabs for CNC cutting, with snap-to-endpoint/midpoint placement
|
||||
- **Contour & Program Editing** — Inline G-code editor with contour reordering, direction arrows, and cut direction reversal
|
||||
- **G-code Output** — Post-process nested layouts to G-code via plugin post-processors
|
||||
- **User-Defined Variables** — Define named variables in G-code (`diameter = 0.3`) referenced with `$name` syntax; Cincinnati post emits numbered machine variables (`#200`) so operators can adjust values at the control
|
||||
- **Built-in Shapes** — 12 parametric shapes (circles, rectangles, L-shapes, T-shapes, flanges, etc.) for quick testing or simple parts
|
||||
- **Interactive Editing** — Zoom, pan, select, clone, push, and manually arrange parts on the plate view
|
||||
- **Pluggable Engine Architecture** — Swap between built-in nesting engines or load custom engines from plugin DLLs
|
||||
@@ -212,7 +213,7 @@ Custom post-processors implement the `IPostProcessor` interface and are auto-dis
|
||||
Nest files (`.nest`) are ZIP archives containing:
|
||||
|
||||
- `nest.json` — JSON metadata: nest info, plate defaults, drawings (with bend data), and plates (with parts and cut-offs)
|
||||
- `programs/program-N` — G-code text for each drawing's cut program
|
||||
- `programs/program-N` — G-code text for each drawing's cut program (may include variable definitions and `$name` references)
|
||||
- `bestfits/bestfit-N` — Cached best-fit pair evaluation results (optional)
|
||||
|
||||
## Roadmap
|
||||
|
||||
Reference in New Issue
Block a user