Compare commits
10
Commits
| 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:
|
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`.
|
- **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).
|
- **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).
|
- **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.
|
- **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`.
|
- **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.
|
- **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.
|
- `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.
|
- `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).
|
- **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)
|
private void EmitContour(Program program, Shape shape, Vector point, Entity entity, ContourType? forceType = null)
|
||||||
{
|
{
|
||||||
var contourType = forceType ?? DetectContourType(shape);
|
var contourType = forceType ?? DetectContourType(shape);
|
||||||
var normal = ComputeNormal(point, entity, contourType);
|
|
||||||
var winding = DetermineWinding(shape);
|
var winding = DetermineWinding(shape);
|
||||||
|
var normal = ComputeNormal(point, entity, contourType, winding);
|
||||||
|
|
||||||
var leadIn = SelectLeadIn(contourType);
|
var leadIn = SelectLeadIn(contourType);
|
||||||
var leadOut = SelectLeadOut(contourType);
|
var leadOut = SelectLeadOut(contourType);
|
||||||
@@ -143,29 +143,33 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
return ContourType.Internal;
|
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;
|
double normal;
|
||||||
|
|
||||||
if (entity is Line line)
|
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);
|
var tangent = line.EndPoint.AngleFrom(line.StartPoint);
|
||||||
normal = tangent + Math.Angle.HalfPI;
|
normal = tangent + Math.Angle.HalfPI;
|
||||||
|
if (winding == RotationType.CCW)
|
||||||
|
normal += System.Math.PI;
|
||||||
}
|
}
|
||||||
else if (entity is Arc arc)
|
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);
|
normal = point.AngleFrom(arc.Center);
|
||||||
|
if (arc.Rotation != winding)
|
||||||
// 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)
|
|
||||||
normal += System.Math.PI;
|
normal += System.Math.PI;
|
||||||
}
|
}
|
||||||
else if (entity is Circle circle)
|
else if (entity is Circle circle)
|
||||||
{
|
{
|
||||||
|
// Radial outward — always correct regardless of winding
|
||||||
normal = point.AngleFrom(circle.Center);
|
normal = point.AngleFrom(circle.Center);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -182,9 +186,10 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
|
|
||||||
public static RotationType DetermineWinding(Shape shape)
|
public static RotationType DetermineWinding(Shape shape)
|
||||||
{
|
{
|
||||||
// Use signed area: positive = CCW, negative = CW
|
if (shape.Entities.Count == 1 && shape.Entities[0] is Circle circle)
|
||||||
var area = shape.Area();
|
return circle.Rotation;
|
||||||
return area >= 0 ? RotationType.CCW : RotationType.CW;
|
|
||||||
|
return shape.ToPolygon().RotationDirection();
|
||||||
}
|
}
|
||||||
|
|
||||||
private LeadIn ClampLeadInForCircle(LeadIn leadIn, Circle circle, Vector contourPoint, double normalAngle)
|
private LeadIn ClampLeadInForCircle(LeadIn leadIn, Circle circle, Vector contourPoint, double normalAngle)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using OpenNest.Converters;
|
using OpenNest.Converters;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using OpenNest.Math;
|
using OpenNest.Math;
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace OpenNest.CNC
|
namespace OpenNest.CNC
|
||||||
@@ -9,6 +10,8 @@ namespace OpenNest.CNC
|
|||||||
{
|
{
|
||||||
public List<ICode> Codes;
|
public List<ICode> Codes;
|
||||||
|
|
||||||
|
public Dictionary<string, VariableDefinition> Variables { get; } = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
private Mode mode;
|
private Mode mode;
|
||||||
|
|
||||||
public Program(Mode mode = Mode.Absolute)
|
public Program(Mode mode = Mode.Absolute)
|
||||||
@@ -454,6 +457,9 @@ namespace OpenNest.CNC
|
|||||||
|
|
||||||
pgm.Codes.AddRange(codes);
|
pgm.Codes.AddRange(codes);
|
||||||
|
|
||||||
|
foreach (var kvp in Variables)
|
||||||
|
pgm.Variables[kvp.Key] = kvp.Value;
|
||||||
|
|
||||||
return pgm;
|
return pgm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+30
-14
@@ -305,6 +305,15 @@ namespace OpenNest.IO
|
|||||||
var writer = new StreamWriter(stream);
|
var writer = new StreamWriter(stream);
|
||||||
writer.AutoFlush = true;
|
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");
|
writer.WriteLine(program.Mode == Mode.Absolute ? "G90" : "G91");
|
||||||
|
|
||||||
for (var i = 0; i < drawing.Program.Length; ++i)
|
for (var i = 0; i < drawing.Program.Length; ++i)
|
||||||
@@ -316,6 +325,13 @@ namespace OpenNest.IO
|
|||||||
stream.Position = 0;
|
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)
|
private string GetCodeString(ICode code)
|
||||||
{
|
{
|
||||||
switch (code.Type)
|
switch (code.Type)
|
||||||
@@ -324,16 +340,16 @@ namespace OpenNest.IO
|
|||||||
{
|
{
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
var arcMove = (ArcMove)code;
|
var arcMove = (ArcMove)code;
|
||||||
|
var refs = arcMove.VariableRefs;
|
||||||
|
|
||||||
var x = System.Math.Round(arcMove.EndPoint.X, OutputPrecision).ToString(CoordinateFormat);
|
var x = FormatCoord(arcMove.EndPoint.X, "X", refs);
|
||||||
var y = System.Math.Round(arcMove.EndPoint.Y, OutputPrecision).ToString(CoordinateFormat);
|
var y = FormatCoord(arcMove.EndPoint.Y, "Y", refs);
|
||||||
var i = System.Math.Round(arcMove.CenterPoint.X, OutputPrecision).ToString(CoordinateFormat);
|
var i = FormatCoord(arcMove.CenterPoint.X, "I", refs);
|
||||||
var j = System.Math.Round(arcMove.CenterPoint.Y, OutputPrecision).ToString(CoordinateFormat);
|
var j = FormatCoord(arcMove.CenterPoint.Y, "J", refs);
|
||||||
|
|
||||||
if (arcMove.Rotation == RotationType.CW)
|
sb.Append(arcMove.Rotation == RotationType.CW
|
||||||
sb.Append(string.Format("G02X{0}Y{1}I{2}J{3}", x, y, i, j));
|
? $"G02X{x}Y{y}I{i}J{j}"
|
||||||
else
|
: $"G03X{x}Y{y}I{i}J{j}");
|
||||||
sb.Append(string.Format("G03X{0}Y{1}I{2}J{3}", x, y, i, j));
|
|
||||||
|
|
||||||
if (arcMove.Layer != LayerType.Cut)
|
if (arcMove.Layer != LayerType.Cut)
|
||||||
sb.Append(GetLayerString(arcMove.Layer));
|
sb.Append(GetLayerString(arcMove.Layer));
|
||||||
@@ -354,10 +370,9 @@ namespace OpenNest.IO
|
|||||||
{
|
{
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
var linearMove = (LinearMove)code;
|
var linearMove = (LinearMove)code;
|
||||||
|
var refs = linearMove.VariableRefs;
|
||||||
|
|
||||||
sb.Append(string.Format("G01X{0}Y{1}",
|
sb.Append($"G01X{FormatCoord(linearMove.EndPoint.X, "X", refs)}Y{FormatCoord(linearMove.EndPoint.Y, "Y", refs)}");
|
||||||
System.Math.Round(linearMove.EndPoint.X, OutputPrecision).ToString(CoordinateFormat),
|
|
||||||
System.Math.Round(linearMove.EndPoint.Y, OutputPrecision).ToString(CoordinateFormat)));
|
|
||||||
|
|
||||||
if (linearMove.Layer != LayerType.Cut)
|
if (linearMove.Layer != LayerType.Cut)
|
||||||
sb.Append(GetLayerString(linearMove.Layer));
|
sb.Append(GetLayerString(linearMove.Layer));
|
||||||
@@ -371,15 +386,16 @@ namespace OpenNest.IO
|
|||||||
case CodeType.RapidMove:
|
case CodeType.RapidMove:
|
||||||
{
|
{
|
||||||
var rapidMove = (RapidMove)code;
|
var rapidMove = (RapidMove)code;
|
||||||
|
var refs = rapidMove.VariableRefs;
|
||||||
|
|
||||||
return string.Format("G00X{0}Y{1}",
|
return $"G00X{FormatCoord(rapidMove.EndPoint.X, "X", refs)}Y{FormatCoord(rapidMove.EndPoint.Y, "Y", refs)}";
|
||||||
System.Math.Round(rapidMove.EndPoint.X, OutputPrecision).ToString(CoordinateFormat),
|
|
||||||
System.Math.Round(rapidMove.EndPoint.Y, OutputPrecision).ToString(CoordinateFormat));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case CodeType.SetFeedrate:
|
case CodeType.SetFeedrate:
|
||||||
{
|
{
|
||||||
var setFeedrate = (Feedrate)code;
|
var setFeedrate = (Feedrate)code;
|
||||||
|
if (setFeedrate.VariableRef != null)
|
||||||
|
return $"F${setFeedrate.VariableRef}";
|
||||||
return "F" + setFeedrate.Value;
|
return "F" + setFeedrate.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
using OpenNest.CNC;
|
using OpenNest.CNC;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
|
using OpenNest.Math;
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace OpenNest.IO
|
namespace OpenNest.IO
|
||||||
@@ -15,6 +19,7 @@ namespace OpenNest.IO
|
|||||||
private CodeSection section;
|
private CodeSection section;
|
||||||
private Program program;
|
private Program program;
|
||||||
private StreamReader reader;
|
private StreamReader reader;
|
||||||
|
private Dictionary<string, double> resolvedVariables;
|
||||||
|
|
||||||
public ProgramReader(Stream stream)
|
public ProgramReader(Stream stream)
|
||||||
{
|
{
|
||||||
@@ -24,11 +29,38 @@ namespace OpenNest.IO
|
|||||||
|
|
||||||
public Program Read()
|
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;
|
string line;
|
||||||
|
|
||||||
while ((line = reader.ReadLine()) != null)
|
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();
|
ProcessCurrentBlock();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,10 +71,43 @@ namespace OpenNest.IO
|
|||||||
{
|
{
|
||||||
var block = new CodeBlock();
|
var block = new CodeBlock();
|
||||||
Code code = null;
|
Code code = null;
|
||||||
for (int i = 0; i < line.Length; ++i)
|
for (var i = 0; i < line.Length; ++i)
|
||||||
{
|
{
|
||||||
var c = line[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)));
|
block.Add((code = new Code(c)));
|
||||||
else if (c == ':')
|
else if (c == ':')
|
||||||
{
|
{
|
||||||
@@ -125,7 +190,10 @@ namespace OpenNest.IO
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'F':
|
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();
|
code = GetNextCode();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -143,6 +211,7 @@ namespace OpenNest.IO
|
|||||||
double y = 0;
|
double y = 0;
|
||||||
var layer = LayerType.Cut;
|
var layer = LayerType.Cut;
|
||||||
var suppressed = false;
|
var suppressed = false;
|
||||||
|
string xRef = null, yRef = null;
|
||||||
|
|
||||||
while (section == CodeSection.Line)
|
while (section == CodeSection.Line)
|
||||||
{
|
{
|
||||||
@@ -157,10 +226,12 @@ namespace OpenNest.IO
|
|||||||
{
|
{
|
||||||
case 'X':
|
case 'X':
|
||||||
x = double.Parse(code.Value);
|
x = double.Parse(code.Value);
|
||||||
|
xRef = code.VariableRef;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'Y':
|
case 'Y':
|
||||||
y = double.Parse(code.Value);
|
y = double.Parse(code.Value);
|
||||||
|
yRef = code.VariableRef;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ':':
|
case ':':
|
||||||
@@ -200,10 +271,13 @@ namespace OpenNest.IO
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var refs = BuildVariableRefs(("X", xRef), ("Y", yRef));
|
||||||
|
|
||||||
if (isRapid)
|
if (isRapid)
|
||||||
program.Codes.Add(new RapidMove(x, y));
|
program.Codes.Add(new RapidMove(x, y) { VariableRefs = refs });
|
||||||
else
|
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)
|
private void ReadArc(RotationType rotation)
|
||||||
@@ -214,6 +288,7 @@ namespace OpenNest.IO
|
|||||||
double j = 0;
|
double j = 0;
|
||||||
var layer = LayerType.Cut;
|
var layer = LayerType.Cut;
|
||||||
var suppressed = false;
|
var suppressed = false;
|
||||||
|
string xRef = null, yRef = null, iRef = null, jRef = null;
|
||||||
|
|
||||||
while (section == CodeSection.Arc)
|
while (section == CodeSection.Arc)
|
||||||
{
|
{
|
||||||
@@ -229,18 +304,22 @@ namespace OpenNest.IO
|
|||||||
{
|
{
|
||||||
case 'X':
|
case 'X':
|
||||||
x = double.Parse(code.Value);
|
x = double.Parse(code.Value);
|
||||||
|
xRef = code.VariableRef;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'Y':
|
case 'Y':
|
||||||
y = double.Parse(code.Value);
|
y = double.Parse(code.Value);
|
||||||
|
yRef = code.VariableRef;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'I':
|
case 'I':
|
||||||
i = double.Parse(code.Value);
|
i = double.Parse(code.Value);
|
||||||
|
iRef = code.VariableRef;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'J':
|
case 'J':
|
||||||
j = double.Parse(code.Value);
|
j = double.Parse(code.Value);
|
||||||
|
jRef = code.VariableRef;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ':':
|
case ':':
|
||||||
@@ -286,7 +365,8 @@ namespace OpenNest.IO
|
|||||||
CenterPoint = new Vector(i, j),
|
CenterPoint = new Vector(i, j),
|
||||||
Rotation = rotation,
|
Rotation = rotation,
|
||||||
Layer = layer,
|
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];
|
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()
|
public void Close()
|
||||||
{
|
{
|
||||||
reader.Close();
|
reader.Close();
|
||||||
@@ -374,6 +627,8 @@ namespace OpenNest.IO
|
|||||||
|
|
||||||
public string Value { get; set; }
|
public string Value { get; set; }
|
||||||
|
|
||||||
|
public string VariableRef { get; set; }
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return Id + Value;
|
return Id + Value;
|
||||||
|
|||||||
@@ -29,6 +29,29 @@ public sealed class FeatureContext
|
|||||||
/// so part-relative programs become plate-absolute under G90.
|
/// so part-relative programs become plate-absolute under G90.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Vector PartLocation { get; set; } = Vector.Zero;
|
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>
|
/// <summary>
|
||||||
@@ -63,7 +86,7 @@ public sealed class CincinnatiFeatureWriter
|
|||||||
var piercePoint = FindPiercePoint(ctx.Codes);
|
var piercePoint = FindPiercePoint(ctx.Codes);
|
||||||
|
|
||||||
// 1. Rapid to pierce point (with line number if configured)
|
// 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
|
// 2. Part name comment on first feature of each part
|
||||||
if (ctx.IsFirstFeatureOfPart && !string.IsNullOrEmpty(ctx.PartName))
|
if (ctx.IsFirstFeatureOfPart && !string.IsNullOrEmpty(ctx.PartName))
|
||||||
@@ -112,7 +135,9 @@ public sealed class CincinnatiFeatureWriter
|
|||||||
kerfEmitted = true;
|
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
|
// Feedrate — etch always uses process feedrate
|
||||||
var feedVar = ctx.IsEtch ? "#148" : GetLinearFeedVariable(linear.Layer);
|
var feedVar = ctx.IsEtch ? "#148" : GetLinearFeedVariable(linear.Layer);
|
||||||
@@ -138,7 +163,9 @@ public sealed class CincinnatiFeatureWriter
|
|||||||
|
|
||||||
// G2 = CW, G3 = CCW
|
// G2 = CW, G3 = CCW
|
||||||
var gCode = arc.Rotation == RotationType.CW ? "G2" : "G3";
|
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
|
// Convert absolute center to incremental I/J
|
||||||
var i = arc.CenterPoint.X - currentPos.X;
|
var i = arc.CenterPoint.X - currentPos.X;
|
||||||
@@ -177,6 +204,52 @@ public sealed class CincinnatiFeatureWriter
|
|||||||
WriteM47(writer, ctx);
|
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)
|
private Vector FindPiercePoint(List<ICode> codes)
|
||||||
{
|
{
|
||||||
foreach (var code in codes)
|
foreach (var code in codes)
|
||||||
@@ -195,14 +268,16 @@ public sealed class CincinnatiFeatureWriter
|
|||||||
return Vector.Zero;
|
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();
|
var sb = new StringBuilder();
|
||||||
|
|
||||||
if (_config.UseLineNumbers)
|
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());
|
writer.WriteLine(sb.ToString());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,7 +66,6 @@ public sealed class CincinnatiPartSubprogramWriter
|
|||||||
_featureWriter.Write(w, ctx);
|
_featureWriter.Write(w, ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
w.WriteLine("G0 X0 Y0");
|
|
||||||
w.WriteLine($"M99 (END OF {drawingName})");
|
w.WriteLine($"M99 (END OF {drawingName})");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -253,6 +253,11 @@ namespace OpenNest.Posts.Cincinnati
|
|||||||
new() { MaxRadius = 4.500, FeedratePercent = 0.80, VariableNumber = 125 }
|
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")]
|
[Category("A. Variables")]
|
||||||
[DisplayName("Sheet Width Variable")]
|
[DisplayName("Sheet Width Variable")]
|
||||||
[Description("Variable number for sheet width.")]
|
[Description("Variable number for sheet width.")]
|
||||||
|
|||||||
@@ -70,7 +70,10 @@ namespace OpenNest.Posts.Cincinnati
|
|||||||
.Where(p => p.Parts.Count > 0)
|
.Where(p => p.Parts.Count > 0)
|
||||||
.ToList();
|
.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 resolver = new MaterialLibraryResolver(Config);
|
||||||
var gas = MaterialLibraryResolver.ResolveGas(nest, Config);
|
var gas = MaterialLibraryResolver.ResolveGas(nest, Config);
|
||||||
var etchLibrary = resolver.ResolveEtchLibrary(Config.DefaultEtchGas);
|
var etchLibrary = resolver.ResolveEtchLibrary(Config.DefaultEtchGas);
|
||||||
@@ -79,42 +82,41 @@ namespace OpenNest.Posts.Cincinnati
|
|||||||
var firstPlate = plates.FirstOrDefault();
|
var firstPlate = plates.FirstOrDefault();
|
||||||
var initialCutLibrary = resolver.ResolveCutLibrary(nest.Material?.Name ?? "", nest.Thickness, gas);
|
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;
|
Dictionary<(int, long), int> partSubprograms = null;
|
||||||
List<(int subNum, string name, Program program)> subprogramEntries = null;
|
List<(int subNum, string name, Program program)> subprogramEntries = null;
|
||||||
|
|
||||||
if (Config.UsePartSubprograms)
|
if (Config.UsePartSubprograms)
|
||||||
(partSubprograms, subprogramEntries) = CincinnatiPartSubprogramWriter.BuildRegistry(plates, Config.PartSubprogramStart);
|
(partSubprograms, subprogramEntries) = CincinnatiPartSubprogramWriter.BuildRegistry(plates, Config.PartSubprogramStart);
|
||||||
|
|
||||||
// 5. Create writers
|
// 6. Create writers
|
||||||
var preamble = new CincinnatiPreambleWriter(Config);
|
var preamble = new CincinnatiPreambleWriter(Config);
|
||||||
var sheetWriter = new CincinnatiSheetWriter(Config, vars);
|
var sheetWriter = new CincinnatiSheetWriter(Config, vars);
|
||||||
|
|
||||||
// 6. Build material description from nest
|
// 7. Build material description from nest
|
||||||
var material = nest.Material;
|
var material = nest.Material;
|
||||||
var materialDesc = material != null
|
var materialDesc = material != null
|
||||||
? $"{material.Name}{(string.IsNullOrEmpty(material.Grade) ? "" : $", {material.Grade}")}"
|
? $"{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);
|
using var writer = new StreamWriter(outputStream, Encoding.UTF8, 1024, leaveOpen: true);
|
||||||
|
|
||||||
// Main program
|
// Main program
|
||||||
preamble.WriteMainProgram(writer, nest.Name ?? "NEST", materialDesc, plates.Count, initialCutLibrary);
|
preamble.WriteMainProgram(writer, nest.Name ?? "NEST", materialDesc, plates, initialCutLibrary);
|
||||||
|
|
||||||
// Variable declaration subprogram
|
// Variable declaration subprogram
|
||||||
preamble.WriteVariableDeclaration(writer, vars);
|
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++)
|
for (var i = 0; i < plates.Count; i++)
|
||||||
{
|
{
|
||||||
var plate = plates[i];
|
var plate = plates[i];
|
||||||
var sheetIndex = i + 1;
|
var layoutIndex = i + 1;
|
||||||
var subNumber = Config.SheetSubprogramStart + i;
|
var subNumber = Config.SheetSubprogramStart + i;
|
||||||
var cutLibrary = resolver.ResolveCutLibrary(nest.Material?.Name ?? "", nest.Thickness, gas);
|
var cutLibrary = resolver.ResolveCutLibrary(nest.Material?.Name ?? "", nest.Thickness, gas);
|
||||||
var isLastSheet = i == plates.Count - 1;
|
sheetWriter.Write(writer, plate, nest.Name ?? "NEST", layoutIndex, subNumber,
|
||||||
sheetWriter.Write(writer, plate, nest.Name ?? "NEST", sheetIndex, subNumber,
|
cutLibrary, etchLibrary, partSubprograms, userVarMapping);
|
||||||
cutLibrary, etchLibrary, partSubprograms, isLastSheet);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Part sub-programs (if enabled)
|
// Part sub-programs (if enabled)
|
||||||
@@ -142,6 +144,103 @@ namespace OpenNest.Posts.Cincinnati
|
|||||||
Post(nest, fs);
|
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()
|
private ProgramVariableManager CreateVariableManager()
|
||||||
{
|
{
|
||||||
var vars = new ProgramVariableManager();
|
var vars = new ProgramVariableManager();
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using OpenNest;
|
using OpenNest;
|
||||||
using OpenNest.CNC;
|
using OpenNest.CNC;
|
||||||
@@ -23,7 +24,7 @@ public sealed class CincinnatiPreambleWriter
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="initialLibrary">Resolved G89 library file for the initial process setup.</param>
|
/// <param name="initialLibrary">Resolved G89 library file for the initial process setup.</param>
|
||||||
public void WriteMainProgram(TextWriter w, string nestName, string materialDescription,
|
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($"NEST {nestName}"));
|
||||||
w.WriteLine(CoordinateFormatter.Comment($"CONFIGURATION - {_config.ConfigurationName}"));
|
w.WriteLine(CoordinateFormatter.Comment($"CONFIGURATION - {_config.ConfigurationName}"));
|
||||||
@@ -54,10 +55,16 @@ public sealed class CincinnatiPreambleWriter
|
|||||||
|
|
||||||
w.WriteLine("GOTO1 (GOTO SHEET NUMBER)");
|
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);
|
var layoutNumber = i + 1;
|
||||||
w.WriteLine($"N{i} M98 P{subNum} (SHEET {i})");
|
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");
|
w.WriteLine("M42");
|
||||||
|
|||||||
@@ -35,10 +35,10 @@ public sealed class CincinnatiSheetWriter
|
|||||||
/// Optional mapping of (drawingId, rotationKey) to sub-program number.
|
/// Optional mapping of (drawingId, rotationKey) to sub-program number.
|
||||||
/// When provided, non-cutoff parts are emitted as M98 calls instead of inline features.
|
/// When provided, non-cutoff parts are emitted as M98 calls instead of inline features.
|
||||||
/// </param>
|
/// </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,
|
string cutLibrary, string etchLibrary,
|
||||||
Dictionary<(int, long), int> partSubprograms = null,
|
Dictionary<(int, long), int> partSubprograms = null,
|
||||||
bool isLastSheet = false)
|
Dictionary<(int drawingId, string varName), int> userVarMapping = null)
|
||||||
{
|
{
|
||||||
if (plate.Parts.Count == 0)
|
if (plate.Parts.Count == 0)
|
||||||
return;
|
return;
|
||||||
@@ -51,11 +51,10 @@ public sealed class CincinnatiSheetWriter
|
|||||||
|
|
||||||
// 1. Sheet header
|
// 1. Sheet header
|
||||||
w.WriteLine("(*****************************************************)");
|
w.WriteLine("(*****************************************************)");
|
||||||
w.WriteLine($"( START OF {nestName}.{sheetIndex:D3} )");
|
w.WriteLine($"( START OF {nestName}.{layoutIndex:D3} )");
|
||||||
w.WriteLine($":{subNumber}");
|
w.WriteLine($":{subNumber}");
|
||||||
w.WriteLine($"( Sheet {sheetIndex} )");
|
w.WriteLine($"( Layout {layoutIndex} )");
|
||||||
w.WriteLine($"( Layout {sheetIndex} )");
|
w.WriteLine($"( SHEET NAME = {_fmt.FormatCoord(width)} X {_fmt.FormatCoord(length)} )");
|
||||||
w.WriteLine($"( SHEET NAME = {_fmt.FormatCoord(length)} X {_fmt.FormatCoord(width)} )");
|
|
||||||
w.WriteLine($"( Total parts on sheet = {partCount} )");
|
w.WriteLine($"( Total parts on sheet = {partCount} )");
|
||||||
w.WriteLine($"#{_config.SheetWidthVariable}={_fmt.FormatCoord(width)} (SHEET WIDTH FOR CUTOFFS)");
|
w.WriteLine($"#{_config.SheetWidthVariable}={_fmt.FormatCoord(width)} (SHEET WIDTH FOR CUTOFFS)");
|
||||||
w.WriteLine($"#{_config.SheetLengthVariable}={_fmt.FormatCoord(length)} (SHEET LENGTH FOR CUTOFFS)");
|
w.WriteLine($"#{_config.SheetLengthVariable}={_fmt.FormatCoord(length)} (SHEET LENGTH FOR CUTOFFS)");
|
||||||
@@ -88,23 +87,22 @@ public sealed class CincinnatiSheetWriter
|
|||||||
|
|
||||||
// 4. Emit parts
|
// 4. Emit parts
|
||||||
if (partSubprograms != null)
|
if (partSubprograms != null)
|
||||||
WritePartsWithSubprograms(w, allParts, cutLibrary, etchLibrary, sheetDiagonal, partSubprograms);
|
WritePartsWithSubprograms(w, allParts, cutLibrary, etchLibrary, sheetDiagonal, width, length, partSubprograms, userVarMapping);
|
||||||
else
|
else
|
||||||
WritePartsInline(w, allParts, cutLibrary, etchLibrary, sheetDiagonal);
|
WritePartsInline(w, allParts, cutLibrary, etchLibrary, sheetDiagonal, width, length, userVarMapping);
|
||||||
|
|
||||||
// 5. Footer
|
// 5. Footer
|
||||||
w.WriteLine("M42");
|
w.WriteLine("M42");
|
||||||
w.WriteLine("G0 X0 Y0");
|
if (_config.PalletExchange != PalletMode.None)
|
||||||
var emitM50 = _config.PalletExchange == PalletMode.EndOfSheet
|
w.WriteLine("M50");
|
||||||
|| (_config.PalletExchange == PalletMode.StartAndEnd && isLastSheet);
|
w.WriteLine($"M99 (END OF {nestName}.{layoutIndex:D3})");
|
||||||
if (emitM50)
|
|
||||||
w.WriteLine($"N{sheetIndex + 1} M50");
|
|
||||||
w.WriteLine($"M99 (END OF {nestName}.{sheetIndex:D3})");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void WritePartsWithSubprograms(TextWriter w, List<Part> allParts,
|
private void WritePartsWithSubprograms(TextWriter w, List<Part> allParts,
|
||||||
string cutLibrary, string etchLibrary, double sheetDiagonal,
|
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 lastPartName = "";
|
||||||
var featureIndex = 0;
|
var featureIndex = 0;
|
||||||
@@ -154,7 +152,12 @@ public sealed class CincinnatiSheetWriter
|
|||||||
LibraryFile = isEtch ? etchLibrary : cutLibrary,
|
LibraryFile = isEtch ? etchLibrary : cutLibrary,
|
||||||
CutDistance = cutDistance,
|
CutDistance = cutDistance,
|
||||||
SheetDiagonal = sheetDiagonal,
|
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);
|
_featureWriter.Write(w, ctx);
|
||||||
@@ -202,7 +205,9 @@ public sealed class CincinnatiSheetWriter
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void WritePartsInline(TextWriter w, List<Part> allParts,
|
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
|
// Split and classify features, ordering etch before cut per part
|
||||||
var features = new List<(Part part, List<ICode> codes, bool isEtch)>();
|
var features = new List<(Part part, List<ICode> codes, bool isEtch)>();
|
||||||
@@ -242,7 +247,12 @@ public sealed class CincinnatiSheetWriter
|
|||||||
LibraryFile = isEtch ? etchLibrary : cutLibrary,
|
LibraryFile = isEtch ? etchLibrary : cutLibrary,
|
||||||
CutDistance = cutDistance,
|
CutDistance = cutDistance,
|
||||||
SheetDiagonal = sheetDiagonal,
|
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);
|
_featureWriter.Write(w, ctx);
|
||||||
|
|||||||
@@ -101,4 +101,35 @@ public class ProgramVariableTests
|
|||||||
move.Offset(5.0, 0);
|
move.Offset(5.0, 0);
|
||||||
Assert.Null(move.VariableRefs);
|
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
|
// Sheet subprogram
|
||||||
Assert.Contains(":101", output);
|
Assert.Contains(":101", output);
|
||||||
Assert.Contains("( Sheet 1 )", output);
|
Assert.Contains("( Layout 1 )", output);
|
||||||
Assert.Contains("G84", output);
|
Assert.Contains("G84", output);
|
||||||
Assert.Contains("M99", output);
|
Assert.Contains("M99", output);
|
||||||
}
|
}
|
||||||
@@ -150,8 +150,8 @@ public class CincinnatiPostProcessorTests
|
|||||||
var output = Encoding.UTF8.GetString(ms.ToArray());
|
var output = Encoding.UTF8.GetString(ms.ToArray());
|
||||||
|
|
||||||
// Should only have one sheet subprogram call in main
|
// Should only have one sheet subprogram call in main
|
||||||
Assert.Contains("N1 M98 P101 (SHEET 1)", output);
|
Assert.Contains("N1 M98 P101 (LAYOUT 1)", output);
|
||||||
Assert.DoesNotContain("SHEET 2", output);
|
Assert.DoesNotContain("LAYOUT 2", output);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -258,8 +258,7 @@ public class CincinnatiPostProcessorTests
|
|||||||
Assert.Contains(":200", output);
|
Assert.Contains(":200", output);
|
||||||
Assert.Contains("G84", output);
|
Assert.Contains("G84", output);
|
||||||
|
|
||||||
// Sub-program ends with G0 X0 Y0 and M99
|
// Sub-program ends with M99
|
||||||
Assert.Contains("G0 X0 Y0", output);
|
|
||||||
Assert.Contains("M99 (END OF Square)", output);
|
Assert.Contains("M99 (END OF Square)", output);
|
||||||
|
|
||||||
// G92 restore after M98 call
|
// G92 restore after M98 call
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using OpenNest.CNC;
|
using OpenNest.CNC;
|
||||||
@@ -19,7 +20,8 @@ public class CincinnatiPreambleWriterTests
|
|||||||
using var sw = new StringWriter(sb);
|
using var sw = new StringWriter(sb);
|
||||||
var writer = new CincinnatiPreambleWriter(config);
|
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();
|
var output = sb.ToString();
|
||||||
Assert.Contains("( NEST TestNest )", output);
|
Assert.Contains("( NEST TestNest )", output);
|
||||||
@@ -29,8 +31,8 @@ public class CincinnatiPreambleWriterTests
|
|||||||
Assert.Contains("G89 PMS135N2PANEL.lib", output);
|
Assert.Contains("G89 PMS135N2PANEL.lib", output);
|
||||||
Assert.Contains("M98 P100 (Variable Declaration)", output);
|
Assert.Contains("M98 P100 (Variable Declaration)", output);
|
||||||
Assert.Contains("GOTO1 (GOTO SHEET NUMBER)", output);
|
Assert.Contains("GOTO1 (GOTO SHEET NUMBER)", output);
|
||||||
Assert.Contains("N1 M98 P101 (SHEET 1)", output);
|
Assert.Contains("N1 M98 P101 (LAYOUT 1)", output);
|
||||||
Assert.Contains("N2 M98 P102 (SHEET 2)", output);
|
Assert.Contains("N2 M98 P102 (LAYOUT 2)", output);
|
||||||
Assert.Contains("M30 (END OF MAIN)", output);
|
Assert.Contains("M30 (END OF MAIN)", output);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,7 +44,7 @@ public class CincinnatiPreambleWriterTests
|
|||||||
using var sw = new StringWriter(sb);
|
using var sw = new StringWriter(sb);
|
||||||
var writer = new CincinnatiPreambleWriter(config);
|
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());
|
Assert.Contains("G21 G90", sb.ToString());
|
||||||
}
|
}
|
||||||
@@ -55,7 +57,7 @@ public class CincinnatiPreambleWriterTests
|
|||||||
using var sw = new StringWriter(sb);
|
using var sw = new StringWriter(sb);
|
||||||
var writer = new CincinnatiPreambleWriter(config);
|
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());
|
Assert.Contains("G20 G90", sb.ToString());
|
||||||
}
|
}
|
||||||
@@ -68,7 +70,7 @@ public class CincinnatiPreambleWriterTests
|
|||||||
using var sw = new StringWriter(sb);
|
using var sw = new StringWriter(sb);
|
||||||
var writer = new CincinnatiPreambleWriter(config);
|
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());
|
Assert.Contains("G121 (SMART RAPIDS)", sb.ToString());
|
||||||
}
|
}
|
||||||
@@ -81,7 +83,7 @@ public class CincinnatiPreambleWriterTests
|
|||||||
using var sw = new StringWriter(sb);
|
using var sw = new StringWriter(sb);
|
||||||
var writer = new CincinnatiPreambleWriter(config);
|
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());
|
Assert.DoesNotContain("G121", sb.ToString());
|
||||||
}
|
}
|
||||||
@@ -94,7 +96,7 @@ public class CincinnatiPreambleWriterTests
|
|||||||
using var sw = new StringWriter(sb);
|
using var sw = new StringWriter(sb);
|
||||||
var writer = new CincinnatiPreambleWriter(config);
|
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());
|
Assert.Contains("M50", sb.ToString());
|
||||||
}
|
}
|
||||||
@@ -107,7 +109,7 @@ public class CincinnatiPreambleWriterTests
|
|||||||
using var sw = new StringWriter(sb);
|
using var sw = new StringWriter(sb);
|
||||||
var writer = new CincinnatiPreambleWriter(config);
|
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());
|
Assert.DoesNotContain("M50", sb.ToString());
|
||||||
}
|
}
|
||||||
@@ -120,7 +122,7 @@ public class CincinnatiPreambleWriterTests
|
|||||||
using var sw = new StringWriter(sb);
|
using var sw = new StringWriter(sb);
|
||||||
var writer = new CincinnatiPreambleWriter(config);
|
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());
|
Assert.Contains("G61", sb.ToString());
|
||||||
}
|
}
|
||||||
@@ -133,11 +135,33 @@ public class CincinnatiPreambleWriterTests
|
|||||||
using var sw = new StringWriter(sb);
|
using var sw = new StringWriter(sb);
|
||||||
var writer = new CincinnatiPreambleWriter(config);
|
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());
|
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]
|
[Fact]
|
||||||
public void WriteVariableDeclaration_EmitsSubprogram()
|
public void WriteVariableDeclaration_EmitsSubprogram()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ public class CincinnatiSheetWriterTests
|
|||||||
|
|
||||||
var output = sb.ToString();
|
var output = sb.ToString();
|
||||||
Assert.Contains(":101", output);
|
Assert.Contains(":101", output);
|
||||||
Assert.Contains("( Sheet 1 )", output);
|
Assert.Contains("( Layout 1 )", output);
|
||||||
Assert.Contains("#110=", output);
|
Assert.Contains("#110=", output);
|
||||||
Assert.Contains("#111=", output);
|
Assert.Contains("#111=", output);
|
||||||
Assert.Contains("G92 X#5021 Y#5022", output);
|
Assert.Contains("G92 X#5021 Y#5022", output);
|
||||||
@@ -55,7 +55,6 @@ public class CincinnatiSheetWriterTests
|
|||||||
|
|
||||||
var output = sb.ToString();
|
var output = sb.ToString();
|
||||||
Assert.Contains("M42", output);
|
Assert.Contains("M42", output);
|
||||||
Assert.Contains("G0 X0 Y0", output);
|
|
||||||
Assert.Contains("M50", output);
|
Assert.Contains("M50", output);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,7 +142,7 @@ public class CincinnatiSheetWriterTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void WriteSheet_StartAndEnd_NoM50OnNonLastSheet()
|
public void WriteSheet_StartAndEnd_EmitsM50()
|
||||||
{
|
{
|
||||||
var config = new CincinnatiPostConfig
|
var config = new CincinnatiPostConfig
|
||||||
{
|
{
|
||||||
@@ -157,33 +156,33 @@ public class CincinnatiSheetWriterTests
|
|||||||
using var sw = new StringWriter(sb);
|
using var sw = new StringWriter(sb);
|
||||||
var sheetWriter = new CincinnatiSheetWriter(config, new ProgramVariableManager());
|
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.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);
|
|
||||||
|
|
||||||
var output = sb.ToString();
|
var output = sb.ToString();
|
||||||
Assert.Contains("M50", output);
|
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]
|
[Fact]
|
||||||
public void WriteSheet_EndOfSheet_AlwaysEmitsM50()
|
public void WriteSheet_EndOfSheet_AlwaysEmitsM50()
|
||||||
{
|
{
|
||||||
@@ -199,7 +198,7 @@ public class CincinnatiSheetWriterTests
|
|||||||
using var sw = new StringWriter(sb);
|
using var sw = new StringWriter(sb);
|
||||||
var sheetWriter = new CincinnatiSheetWriter(config, new ProgramVariableManager());
|
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();
|
var output = sb.ToString();
|
||||||
Assert.Contains("M50", output);
|
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;
|
snapPoint = closest;
|
||||||
snapEntity = entity;
|
snapEntity = entity;
|
||||||
snapContourType = info.ContourType;
|
snapContourType = info.ContourType;
|
||||||
snapNormal = ContourCuttingStrategy.ComputeNormal(closest, entity, info.ContourType);
|
snapNormal = ContourCuttingStrategy.ComputeNormal(closest, entity, info.ContourType, info.Winding);
|
||||||
hasSnap = true;
|
hasSnap = true;
|
||||||
hoveredContour = info;
|
hoveredContour = info;
|
||||||
}
|
}
|
||||||
@@ -282,7 +282,7 @@ namespace OpenNest.Actions
|
|||||||
{
|
{
|
||||||
snapPoint = bestPoint;
|
snapPoint = bestPoint;
|
||||||
snapEntity = bestEntity;
|
snapEntity = bestEntity;
|
||||||
snapNormal = ContourCuttingStrategy.ComputeNormal(bestPoint, bestEntity, snapContourType);
|
snapNormal = ContourCuttingStrategy.ComputeNormal(bestPoint, bestEntity, snapContourType, hoveredContour.Winding);
|
||||||
activeSnapType = bestType;
|
activeSnapType = bestType;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,7 +356,8 @@ namespace OpenNest.Actions
|
|||||||
contours.Add(new ShapeInfo
|
contours.Add(new ShapeInfo
|
||||||
{
|
{
|
||||||
Shape = profile.Perimeter,
|
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
|
contours.Add(new ShapeInfo
|
||||||
{
|
{
|
||||||
Shape = cutout,
|
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 Shape Shape { get; set; }
|
||||||
public ContourType ContourType { 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
|
- **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
|
- **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
|
- **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
|
- **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
|
- **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
|
- **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 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)
|
- `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)
|
- `bestfits/bestfit-N` — Cached best-fit pair evaluation results (optional)
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|||||||
Reference in New Issue
Block a user