Compare commits
24 Commits
a6e2845261
...
e50a7c82cf
| Author | SHA1 | Date | |
|---|---|---|---|
| e50a7c82cf | |||
| 7a893ef50f | |||
| 925a1c7751 | |||
| 036b48e273 | |||
| bd9b0369cf | |||
| 93391c4b8f | |||
| ebab795f86 | |||
| 9f9111975d | |||
| 25ee193ae6 | |||
| 5bcad9667b | |||
| 64945220b9 | |||
| ec0baad585 | |||
| f26edb824d | |||
| aae593a73e | |||
| 36d8f7fb11 | |||
| 52ad5b4575 | |||
| 7416f8ae3f | |||
| 46e3104dfc | |||
| 27afa04e4a | |||
| 95b9613e2d | |||
| 3bc9301e22 | |||
| 1040db414f | |||
| 287023d802 | |||
| 3a24e76dbd |
@@ -24,10 +24,10 @@ Eight projects form a layered architecture:
|
||||
Domain model, geometry, and CNC primitives organized into namespaces:
|
||||
|
||||
- **Root** (`namespace OpenNest`): Domain model — `Nest` → `Plate[]` → `Part[]` → `Drawing` → `Program`. A `Nest` is the top-level container. Each `Plate` has a size, material, quadrant, spacing, and contains placed `Part` instances. Each `Part` references a `Drawing` (the template) and has its own location/rotation. A `Drawing` wraps a CNC `Program`. Also contains utilities: `PartGeometry`, `Align`, `Sequence`, `Timing`.
|
||||
- **CNC** (`CNC/`, `namespace OpenNest.CNC`): `Program` holds a list of `ICode` instructions (G-code-like: `RapidMove`, `LinearMove`, `ArcMove`, `SubProgramCall`). Programs support absolute/incremental mode conversion, rotation, offset, bounding box calculation, and cloning.
|
||||
- **CNC** (`CNC/`, `namespace OpenNest.CNC`): `Program` holds a list of `ICode` instructions (G-code-like: `RapidMove`, `LinearMove`, `ArcMove`, `SubProgramCall`) and an optional `Variables` dictionary of `VariableDefinition` entries. Programs support absolute/incremental mode conversion, rotation, offset, bounding box calculation, and cloning. `VariableDefinition` stores a named variable's expression, resolved value, and flags (`Inline`, `Global`). `ProgramVariableManager` manages numbered machine variables for post-processor output.
|
||||
- **Geometry** (`Geometry/`, `namespace OpenNest.Geometry`): Spatial primitives (`Vector`, `Box`, `Size`, `Spacing`, `BoundingBox`, `IBoundable`) and higher-level shapes (`Line`, `Arc`, `Circle`, `Polygon`, `Shape`) used for intersection detection, area calculation, and DXF conversion. Also contains `Intersect` (intersection algorithms), `ShapeBuilder` (entity chaining), `GeometryOptimizer` (line/arc merging), `SpatialQuery` (directional distance, ray casting, box queries), `ShapeProfile` (perimeter/area analysis), `NoFitPolygon`, `InnerFitPolygon`, `ConvexHull`, `ConvexDecomposition`, `RotatingCalipers`, and `Collision` (overlap detection with Sutherland-Hodgman polygon clipping and hole subtraction).
|
||||
- **Converters** (`Converters/`, `namespace OpenNest.Converters`): Bridges between CNC and Geometry — `ConvertProgram` (CNC→Geometry), `ConvertGeometry` (Geometry→CNC), `ConvertMode` (absolute↔incremental).
|
||||
- **Math** (`Math/`, `namespace OpenNest.Math`): `Angle` (radian/degree conversion), `Tolerance` (floating-point comparison), `Trigonometry`, `Generic` (swap utility), `EvenOdd`, `Rounding` (factor-based rounding). Note: `OpenNest.Math` shadows `System.Math` — use `System.Math` fully qualified where both are needed.
|
||||
- **Math** (`Math/`, `namespace OpenNest.Math`): `Angle` (radian/degree conversion), `Tolerance` (floating-point comparison), `Trigonometry`, `Generic` (swap utility), `EvenOdd`, `Rounding` (factor-based rounding), `ExpressionEvaluator` (arithmetic expression parser for G-code variable expressions with `$name` references). Note: `OpenNest.Math` shadows `System.Math` — use `System.Math` fully qualified where both are needed.
|
||||
- **CNC/CuttingStrategy** (`CNC/CuttingStrategy/`, `namespace OpenNest.CNC`): `ContourCuttingStrategy` orchestrates cut ordering, lead-ins/lead-outs, and tabs. Includes `LeadIn`/`LeadOut` hierarchies (line, arc, clean-hole variants), `Tab` hierarchy (normal, machine, breaker), and `CuttingParameters`/`AssignmentParameters`/`SequenceParameters` configuration.
|
||||
- **Collections** (`Collections/`, `namespace OpenNest.Collections`): `ObservableList<T>`, `DrawingCollection`.
|
||||
- **CutOffs** (`namespace OpenNest`): `CutOff` (axis-aligned cut line with position, axis, optional start/end limits), `CutOffAxis` enum (`Horizontal`, `Vertical`), `CutOffSettings` (clearance, overtravel, min segment length, direction), `CutDirection` enum (`TowardOrigin`, `AwayFromOrigin`). Cut-offs generate CNC `Program` objects with trimmed line segments that avoid parts.
|
||||
@@ -116,3 +116,4 @@ Always keep `README.md` and `CLAUDE.md` up to date when making changes that affe
|
||||
- `Compactor` performs post-fill gravity compaction — after filling, parts are pushed toward a plate edge using directional distance calculations to close gaps between irregular shapes.
|
||||
- `FillScore` uses lexicographic comparison (count > utilization > compactness) to rank fill results consistently across all fill strategies.
|
||||
- **Cut-off materialization lifecycle**: `CutOff` objects live on `Plate.CutOffs`. Each generates a `Drawing` (with `IsCutOff = true`) whose `Program` contains trimmed line segments. `Plate.RegenerateCutOffs(settings)` removes old cut-off Parts, recomputes programs, and re-adds them to `Plate.Parts`. Regeneration triggers: cut-off add/remove/move, part drag complete, fill complete, plate transform. Cut-off Parts are excluded from quantity tracking, utilization, overlap detection, and nest file serialization (programs are regenerated from definitions on load).
|
||||
- **User-defined G-code variables**: Programs can contain named variable definitions (`name = expression [inline] [global]`) referenced in coordinates with `$name`. Variables resolve to doubles at parse time for geometry/nesting. `VariableRefs` on `Motion`/`Feedrate` track the symbolic link so post processors can emit machine variable references. Cincinnati post maps non-inline variables to numbered machine variables (`#200+`) with descriptive comments. Global variables share a number across programs; local variables get per-drawing numbers. `ProgramReader` uses a two-pass parse (collect definitions, then parse G-code with substitution). `NestWriter` serializes definitions and `$references` back to text for round-trip fidelity.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.CNC
|
||||
{
|
||||
@@ -66,7 +67,8 @@ namespace OpenNest.CNC
|
||||
return new ArcMove(EndPoint, CenterPoint, Rotation)
|
||||
{
|
||||
Layer = Layer,
|
||||
Suppressed = Suppressed
|
||||
Suppressed = Suppressed,
|
||||
VariableRefs = VariableRefs != null ? new Dictionary<string, string>(VariableRefs) : null
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,126 @@ namespace OpenNest.CNC.CuttingStrategy
|
||||
};
|
||||
}
|
||||
|
||||
public CuttingResult ApplySingle(Program partProgram, Vector point, Entity entity, ContourType contourType)
|
||||
{
|
||||
var entities = partProgram.ToGeometry();
|
||||
entities.RemoveAll(e => e.Layer == SpecialLayers.Rapid);
|
||||
|
||||
var scribeEntities = entities.FindAll(e => e.Layer == SpecialLayers.Scribe);
|
||||
entities.RemoveAll(e => e.Layer == SpecialLayers.Scribe);
|
||||
|
||||
var profile = new ShapeProfile(entities);
|
||||
|
||||
var result = new Program(Mode.Absolute);
|
||||
|
||||
EmitScribeContours(result, scribeEntities);
|
||||
|
||||
// Find the target shape that contains the clicked entity
|
||||
var (targetShape, matchedEntity) = FindTargetShape(profile, point, entity);
|
||||
|
||||
// Emit cutouts — only the target gets lead-in/out
|
||||
foreach (var cutout in profile.Cutouts)
|
||||
{
|
||||
if (cutout == targetShape)
|
||||
{
|
||||
var ct = DetectContourType(cutout);
|
||||
EmitContour(result, cutout, point, matchedEntity, ct);
|
||||
}
|
||||
else
|
||||
{
|
||||
EmitRawContour(result, cutout);
|
||||
}
|
||||
}
|
||||
|
||||
// Emit perimeter
|
||||
if (profile.Perimeter == targetShape)
|
||||
{
|
||||
EmitContour(result, profile.Perimeter, point, matchedEntity, ContourType.External);
|
||||
}
|
||||
else
|
||||
{
|
||||
EmitRawContour(result, profile.Perimeter);
|
||||
}
|
||||
|
||||
result.Mode = Mode.Incremental;
|
||||
|
||||
return new CuttingResult
|
||||
{
|
||||
Program = result,
|
||||
LastCutPoint = point
|
||||
};
|
||||
}
|
||||
|
||||
private static (Shape Shape, Entity Entity) FindTargetShape(ShapeProfile profile, Vector point, Entity clickedEntity)
|
||||
{
|
||||
var matched = FindMatchingEntity(profile.Perimeter, clickedEntity);
|
||||
if (matched != null)
|
||||
return (profile.Perimeter, matched);
|
||||
|
||||
foreach (var cutout in profile.Cutouts)
|
||||
{
|
||||
matched = FindMatchingEntity(cutout, clickedEntity);
|
||||
if (matched != null)
|
||||
return (cutout, matched);
|
||||
}
|
||||
|
||||
// Fallback: closest shape, use closest point to find entity
|
||||
var best = profile.Perimeter;
|
||||
var bestPt = profile.Perimeter.ClosestPointTo(point, out var bestEntity);
|
||||
var bestDist = bestPt.DistanceTo(point);
|
||||
|
||||
foreach (var cutout in profile.Cutouts)
|
||||
{
|
||||
var pt = cutout.ClosestPointTo(point, out var cutoutEntity);
|
||||
var dist = pt.DistanceTo(point);
|
||||
if (dist < bestDist)
|
||||
{
|
||||
best = cutout;
|
||||
bestEntity = cutoutEntity;
|
||||
bestDist = dist;
|
||||
}
|
||||
}
|
||||
|
||||
return (best, bestEntity);
|
||||
}
|
||||
|
||||
private static Entity FindMatchingEntity(Shape shape, Entity clickedEntity)
|
||||
{
|
||||
foreach (var shapeEntity in shape.Entities)
|
||||
{
|
||||
if (shapeEntity.GetType() != clickedEntity.GetType())
|
||||
continue;
|
||||
|
||||
if (shapeEntity is Line sLine && clickedEntity is Line cLine)
|
||||
{
|
||||
if (sLine.StartPoint.DistanceTo(cLine.StartPoint) < Math.Tolerance.Epsilon
|
||||
&& sLine.EndPoint.DistanceTo(cLine.EndPoint) < Math.Tolerance.Epsilon)
|
||||
return shapeEntity;
|
||||
}
|
||||
else if (shapeEntity is Arc sArc && clickedEntity is Arc cArc)
|
||||
{
|
||||
if (System.Math.Abs(sArc.Radius - cArc.Radius) < Math.Tolerance.Epsilon
|
||||
&& sArc.Center.DistanceTo(cArc.Center) < Math.Tolerance.Epsilon)
|
||||
return shapeEntity;
|
||||
}
|
||||
else if (shapeEntity is Circle sCircle && clickedEntity is Circle cCircle)
|
||||
{
|
||||
if (System.Math.Abs(sCircle.Radius - cCircle.Radius) < Math.Tolerance.Epsilon
|
||||
&& sCircle.Center.DistanceTo(cCircle.Center) < Math.Tolerance.Epsilon)
|
||||
return shapeEntity;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void EmitRawContour(Program program, Shape shape)
|
||||
{
|
||||
var startPoint = GetShapeStartPoint(shape);
|
||||
program.Codes.Add(new RapidMove(startPoint));
|
||||
program.Codes.AddRange(ConvertShapeToMoves(shape, startPoint));
|
||||
}
|
||||
|
||||
private static List<ContourEntry> ResolveLeadInPoints(List<Shape> cutouts, Vector startPoint)
|
||||
{
|
||||
var entries = new ContourEntry[cutouts.Count];
|
||||
@@ -70,8 +190,8 @@ namespace OpenNest.CNC.CuttingStrategy
|
||||
private void EmitContour(Program program, Shape shape, Vector point, Entity entity, ContourType? forceType = null)
|
||||
{
|
||||
var contourType = forceType ?? DetectContourType(shape);
|
||||
var normal = ComputeNormal(point, entity, contourType);
|
||||
var winding = DetermineWinding(shape);
|
||||
var normal = ComputeNormal(point, entity, contourType, winding);
|
||||
|
||||
var leadIn = SelectLeadIn(contourType);
|
||||
var leadOut = SelectLeadOut(contourType);
|
||||
@@ -143,29 +263,33 @@ namespace OpenNest.CNC.CuttingStrategy
|
||||
return ContourType.Internal;
|
||||
}
|
||||
|
||||
public static double ComputeNormal(Vector point, Entity entity, ContourType contourType)
|
||||
public static double ComputeNormal(Vector point, Entity entity, ContourType contourType,
|
||||
RotationType winding = RotationType.CW)
|
||||
{
|
||||
double normal;
|
||||
|
||||
if (entity is Line line)
|
||||
{
|
||||
// Perpendicular to line direction
|
||||
// Perpendicular to line direction: tangent + π/2 = left side.
|
||||
// Left side = outward for CW winding; for CCW winding, outward
|
||||
// is on the right side, so flip.
|
||||
var tangent = line.EndPoint.AngleFrom(line.StartPoint);
|
||||
normal = tangent + Math.Angle.HalfPI;
|
||||
if (winding == RotationType.CCW)
|
||||
normal += System.Math.PI;
|
||||
}
|
||||
else if (entity is Arc arc)
|
||||
{
|
||||
// Radial direction from center to point
|
||||
// Radial direction from center to point.
|
||||
// Flip when the arc direction differs from the contour winding —
|
||||
// that indicates a concave feature where radial points inward.
|
||||
normal = point.AngleFrom(arc.Center);
|
||||
|
||||
// For CCW arcs the radial points the wrong way — flip it.
|
||||
// CW arcs are convex features (corners) where radial = outward.
|
||||
// CCW arcs are concave features (slots) where radial = inward.
|
||||
if (arc.Rotation == RotationType.CCW)
|
||||
if (arc.Rotation != winding)
|
||||
normal += System.Math.PI;
|
||||
}
|
||||
else if (entity is Circle circle)
|
||||
{
|
||||
// Radial outward — always correct regardless of winding
|
||||
normal = point.AngleFrom(circle.Center);
|
||||
}
|
||||
else
|
||||
@@ -182,9 +306,10 @@ namespace OpenNest.CNC.CuttingStrategy
|
||||
|
||||
public static RotationType DetermineWinding(Shape shape)
|
||||
{
|
||||
// Use signed area: positive = CCW, negative = CW
|
||||
var area = shape.Area();
|
||||
return area >= 0 ? RotationType.CCW : RotationType.CW;
|
||||
if (shape.Entities.Count == 1 && shape.Entities[0] is Circle circle)
|
||||
return circle.Rotation;
|
||||
|
||||
return shape.ToPolygon().RotationDirection();
|
||||
}
|
||||
|
||||
private LeadIn ClampLeadInForCircle(LeadIn leadIn, Circle circle, Vector contourPoint, double normalAngle)
|
||||
|
||||
@@ -23,6 +23,9 @@ namespace OpenNest.CNC.CuttingStrategy
|
||||
|
||||
public double PierceClearance { get; set; } = 0.0625;
|
||||
|
||||
public double AutoTabMinSize { get; set; }
|
||||
public double AutoTabMaxSize { get; set; }
|
||||
|
||||
public Tab TabConfig { get; set; }
|
||||
public bool TabsEnabled { get; set; }
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
|
||||
public double Value { get; set; }
|
||||
|
||||
public string VariableRef { get; set; }
|
||||
|
||||
public CodeType Type
|
||||
{
|
||||
get { return CodeType.SetFeedrate; }
|
||||
@@ -24,7 +26,7 @@
|
||||
|
||||
public ICode Clone()
|
||||
{
|
||||
return new Feedrate(Value);
|
||||
return new Feedrate(Value) { VariableRef = VariableRef };
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.CNC
|
||||
{
|
||||
@@ -32,7 +33,8 @@ namespace OpenNest.CNC
|
||||
return new LinearMove(EndPoint)
|
||||
{
|
||||
Layer = Layer,
|
||||
Suppressed = Suppressed
|
||||
Suppressed = Suppressed,
|
||||
VariableRefs = VariableRefs != null ? new Dictionary<string, string>(VariableRefs) : null
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.CNC
|
||||
{
|
||||
@@ -14,6 +15,8 @@ namespace OpenNest.CNC
|
||||
|
||||
public bool Suppressed { get; set; }
|
||||
|
||||
public Dictionary<string, string> VariableRefs { get; set; }
|
||||
|
||||
protected Motion()
|
||||
{
|
||||
Feedrate = CNC.Feedrate.UseDefault;
|
||||
@@ -22,21 +25,25 @@ namespace OpenNest.CNC
|
||||
public virtual void Rotate(double angle)
|
||||
{
|
||||
EndPoint = EndPoint.Rotate(angle);
|
||||
VariableRefs = null;
|
||||
}
|
||||
|
||||
public virtual void Rotate(double angle, Vector origin)
|
||||
{
|
||||
EndPoint = EndPoint.Rotate(angle, origin);
|
||||
VariableRefs = null;
|
||||
}
|
||||
|
||||
public virtual void Offset(double x, double y)
|
||||
{
|
||||
EndPoint = new Vector(EndPoint.X + x, EndPoint.Y + y);
|
||||
VariableRefs = null;
|
||||
}
|
||||
|
||||
public virtual void Offset(Vector voffset)
|
||||
{
|
||||
EndPoint += voffset;
|
||||
VariableRefs = null;
|
||||
}
|
||||
|
||||
public abstract CodeType Type { get; }
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.CNC
|
||||
@@ -9,6 +10,8 @@ namespace OpenNest.CNC
|
||||
{
|
||||
public List<ICode> Codes;
|
||||
|
||||
public Dictionary<string, VariableDefinition> Variables { get; } = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private Mode mode;
|
||||
|
||||
public Program(Mode mode = Mode.Absolute)
|
||||
@@ -454,6 +457,9 @@ namespace OpenNest.CNC
|
||||
|
||||
pgm.Codes.AddRange(codes);
|
||||
|
||||
foreach (var kvp in Variables)
|
||||
pgm.Variables[kvp.Key] = kvp.Value;
|
||||
|
||||
return pgm;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.CNC
|
||||
{
|
||||
@@ -28,7 +29,8 @@ namespace OpenNest.CNC
|
||||
{
|
||||
return new RapidMove(EndPoint)
|
||||
{
|
||||
Suppressed = Suppressed
|
||||
Suppressed = Suppressed,
|
||||
VariableRefs = VariableRefs != null ? new Dictionary<string, string>(VariableRefs) : null
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace OpenNest.CNC
|
||||
{
|
||||
public sealed class VariableDefinition
|
||||
{
|
||||
public string Name { get; }
|
||||
public string Expression { get; }
|
||||
public double Value { get; }
|
||||
public bool Inline { get; }
|
||||
public bool Global { get; }
|
||||
|
||||
public VariableDefinition(string name, string expression, double value,
|
||||
bool inline = false, bool global = false)
|
||||
{
|
||||
Name = name;
|
||||
Expression = expression;
|
||||
Value = value;
|
||||
Inline = inline;
|
||||
Global = global;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
|
||||
namespace OpenNest.Math
|
||||
{
|
||||
/// <summary>
|
||||
/// Recursive descent parser for simple arithmetic expressions supporting
|
||||
/// +, -, *, /, parentheses, unary minus/plus, and $variable references.
|
||||
/// </summary>
|
||||
public static class ExpressionEvaluator
|
||||
{
|
||||
public static double Evaluate(string expression, IReadOnlyDictionary<string, double> variables)
|
||||
{
|
||||
var parser = new Parser(expression, variables);
|
||||
var result = parser.ParseExpression();
|
||||
parser.SkipWhitespace();
|
||||
if (!parser.IsEnd)
|
||||
throw new FormatException($"Unexpected character at position {parser.Position}: '{parser.Current}'");
|
||||
return result;
|
||||
}
|
||||
|
||||
private ref struct Parser
|
||||
{
|
||||
private readonly ReadOnlySpan<char> _input;
|
||||
private readonly IReadOnlyDictionary<string, double> _variables;
|
||||
private int _pos;
|
||||
|
||||
public Parser(string input, IReadOnlyDictionary<string, double> variables)
|
||||
{
|
||||
_input = input.AsSpan();
|
||||
_variables = variables;
|
||||
_pos = 0;
|
||||
}
|
||||
|
||||
public int Position => _pos;
|
||||
public bool IsEnd => _pos >= _input.Length;
|
||||
public char Current => _input[_pos];
|
||||
|
||||
public void SkipWhitespace()
|
||||
{
|
||||
while (_pos < _input.Length && _input[_pos] == ' ')
|
||||
_pos++;
|
||||
}
|
||||
|
||||
// Expression = Term (('+' | '-') Term)*
|
||||
public double ParseExpression()
|
||||
{
|
||||
SkipWhitespace();
|
||||
var left = ParseTerm();
|
||||
|
||||
while (true)
|
||||
{
|
||||
SkipWhitespace();
|
||||
if (IsEnd) break;
|
||||
|
||||
var op = Current;
|
||||
if (op != '+' && op != '-') break;
|
||||
|
||||
_pos++;
|
||||
SkipWhitespace();
|
||||
var right = ParseTerm();
|
||||
|
||||
left = op == '+' ? left + right : left - right;
|
||||
}
|
||||
|
||||
return left;
|
||||
}
|
||||
|
||||
// Term = Unary (('*' | '/') Unary)*
|
||||
private double ParseTerm()
|
||||
{
|
||||
var left = ParseUnary();
|
||||
|
||||
while (true)
|
||||
{
|
||||
SkipWhitespace();
|
||||
if (IsEnd) break;
|
||||
|
||||
var op = Current;
|
||||
if (op != '*' && op != '/') break;
|
||||
|
||||
_pos++;
|
||||
SkipWhitespace();
|
||||
var right = ParseUnary();
|
||||
|
||||
left = op == '*' ? left * right : left / right;
|
||||
}
|
||||
|
||||
return left;
|
||||
}
|
||||
|
||||
// Unary = ('-' | '+')? Primary
|
||||
private double ParseUnary()
|
||||
{
|
||||
SkipWhitespace();
|
||||
if (!IsEnd && Current == '-')
|
||||
{
|
||||
_pos++;
|
||||
return -ParsePrimary();
|
||||
}
|
||||
if (!IsEnd && Current == '+')
|
||||
{
|
||||
_pos++;
|
||||
}
|
||||
return ParsePrimary();
|
||||
}
|
||||
|
||||
// Primary = '(' Expression ')' | '$' Identifier | Number
|
||||
private double ParsePrimary()
|
||||
{
|
||||
SkipWhitespace();
|
||||
|
||||
if (IsEnd)
|
||||
throw new FormatException("Unexpected end of expression.");
|
||||
|
||||
if (Current == '(')
|
||||
{
|
||||
_pos++; // consume '('
|
||||
var value = ParseExpression();
|
||||
SkipWhitespace();
|
||||
if (IsEnd || Current != ')')
|
||||
throw new FormatException("Expected closing parenthesis.");
|
||||
_pos++; // consume ')'
|
||||
return value;
|
||||
}
|
||||
|
||||
if (Current == '$')
|
||||
{
|
||||
_pos++; // consume '$'
|
||||
var start = _pos;
|
||||
while (_pos < _input.Length && (char.IsLetterOrDigit(_input[_pos]) || _input[_pos] == '_'))
|
||||
_pos++;
|
||||
if (_pos == start)
|
||||
throw new FormatException("Expected variable name after '$'.");
|
||||
var name = _input.Slice(start, _pos - start).ToString();
|
||||
if (!_variables.TryGetValue(name, out var varValue))
|
||||
throw new KeyNotFoundException($"Undefined variable: ${name}");
|
||||
return varValue;
|
||||
}
|
||||
|
||||
// Number
|
||||
var numStart = _pos;
|
||||
while (_pos < _input.Length && (char.IsDigit(_input[_pos]) || _input[_pos] == '.'))
|
||||
_pos++;
|
||||
|
||||
if (_pos == numStart)
|
||||
throw new FormatException($"Unexpected character '{Current}' at position {_pos}.");
|
||||
|
||||
var numSpan = _input.Slice(numStart, _pos - numStart).ToString();
|
||||
if (!double.TryParse(numSpan, NumberStyles.Float, CultureInfo.InvariantCulture, out var number))
|
||||
throw new FormatException($"Invalid number: '{numSpan}'");
|
||||
|
||||
return number;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,6 +72,18 @@ namespace OpenNest
|
||||
UpdateBounds();
|
||||
}
|
||||
|
||||
public void ApplySingleLeadIn(CNC.CuttingStrategy.CuttingParameters parameters,
|
||||
Geometry.Vector point, Geometry.Entity entity, CNC.CuttingStrategy.ContourType contourType)
|
||||
{
|
||||
preLeadInRotation = Rotation;
|
||||
var strategy = new CNC.CuttingStrategy.ContourCuttingStrategy { Parameters = parameters };
|
||||
var result = strategy.ApplySingle(Program, point, entity, contourType);
|
||||
Program = result.Program;
|
||||
CuttingParameters = parameters;
|
||||
HasManualLeadIns = true;
|
||||
UpdateBounds();
|
||||
}
|
||||
|
||||
public void RemoveLeadIns()
|
||||
{
|
||||
var rotation = preLeadInRotation;
|
||||
|
||||
+30
-14
@@ -305,6 +305,15 @@ namespace OpenNest.IO
|
||||
var writer = new StreamWriter(stream);
|
||||
writer.AutoFlush = true;
|
||||
|
||||
// Emit variable definitions before G-code
|
||||
foreach (var v in program.Variables.Values)
|
||||
{
|
||||
var line = $"{v.Name} = {v.Expression}";
|
||||
if (v.Inline) line += " inline";
|
||||
if (v.Global) line += " global";
|
||||
writer.WriteLine(line);
|
||||
}
|
||||
|
||||
writer.WriteLine(program.Mode == Mode.Absolute ? "G90" : "G91");
|
||||
|
||||
for (var i = 0; i < drawing.Program.Length; ++i)
|
||||
@@ -316,6 +325,13 @@ namespace OpenNest.IO
|
||||
stream.Position = 0;
|
||||
}
|
||||
|
||||
private string FormatCoord(double value, string axis, Dictionary<string, string> variableRefs)
|
||||
{
|
||||
if (variableRefs != null && variableRefs.TryGetValue(axis, out var varName))
|
||||
return $"${varName}";
|
||||
return System.Math.Round(value, OutputPrecision).ToString(CoordinateFormat);
|
||||
}
|
||||
|
||||
private string GetCodeString(ICode code)
|
||||
{
|
||||
switch (code.Type)
|
||||
@@ -324,16 +340,16 @@ namespace OpenNest.IO
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
var arcMove = (ArcMove)code;
|
||||
var refs = arcMove.VariableRefs;
|
||||
|
||||
var x = System.Math.Round(arcMove.EndPoint.X, OutputPrecision).ToString(CoordinateFormat);
|
||||
var y = System.Math.Round(arcMove.EndPoint.Y, OutputPrecision).ToString(CoordinateFormat);
|
||||
var i = System.Math.Round(arcMove.CenterPoint.X, OutputPrecision).ToString(CoordinateFormat);
|
||||
var j = System.Math.Round(arcMove.CenterPoint.Y, OutputPrecision).ToString(CoordinateFormat);
|
||||
var x = FormatCoord(arcMove.EndPoint.X, "X", refs);
|
||||
var y = FormatCoord(arcMove.EndPoint.Y, "Y", refs);
|
||||
var i = FormatCoord(arcMove.CenterPoint.X, "I", refs);
|
||||
var j = FormatCoord(arcMove.CenterPoint.Y, "J", refs);
|
||||
|
||||
if (arcMove.Rotation == RotationType.CW)
|
||||
sb.Append(string.Format("G02X{0}Y{1}I{2}J{3}", x, y, i, j));
|
||||
else
|
||||
sb.Append(string.Format("G03X{0}Y{1}I{2}J{3}", x, y, i, j));
|
||||
sb.Append(arcMove.Rotation == RotationType.CW
|
||||
? $"G02X{x}Y{y}I{i}J{j}"
|
||||
: $"G03X{x}Y{y}I{i}J{j}");
|
||||
|
||||
if (arcMove.Layer != LayerType.Cut)
|
||||
sb.Append(GetLayerString(arcMove.Layer));
|
||||
@@ -354,10 +370,9 @@ namespace OpenNest.IO
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
var linearMove = (LinearMove)code;
|
||||
var refs = linearMove.VariableRefs;
|
||||
|
||||
sb.Append(string.Format("G01X{0}Y{1}",
|
||||
System.Math.Round(linearMove.EndPoint.X, OutputPrecision).ToString(CoordinateFormat),
|
||||
System.Math.Round(linearMove.EndPoint.Y, OutputPrecision).ToString(CoordinateFormat)));
|
||||
sb.Append($"G01X{FormatCoord(linearMove.EndPoint.X, "X", refs)}Y{FormatCoord(linearMove.EndPoint.Y, "Y", refs)}");
|
||||
|
||||
if (linearMove.Layer != LayerType.Cut)
|
||||
sb.Append(GetLayerString(linearMove.Layer));
|
||||
@@ -371,15 +386,16 @@ namespace OpenNest.IO
|
||||
case CodeType.RapidMove:
|
||||
{
|
||||
var rapidMove = (RapidMove)code;
|
||||
var refs = rapidMove.VariableRefs;
|
||||
|
||||
return string.Format("G00X{0}Y{1}",
|
||||
System.Math.Round(rapidMove.EndPoint.X, OutputPrecision).ToString(CoordinateFormat),
|
||||
System.Math.Round(rapidMove.EndPoint.Y, OutputPrecision).ToString(CoordinateFormat));
|
||||
return $"G00X{FormatCoord(rapidMove.EndPoint.X, "X", refs)}Y{FormatCoord(rapidMove.EndPoint.Y, "Y", refs)}";
|
||||
}
|
||||
|
||||
case CodeType.SetFeedrate:
|
||||
{
|
||||
var setFeedrate = (Feedrate)code;
|
||||
if (setFeedrate.VariableRef != null)
|
||||
return $"F${setFeedrate.VariableRef}";
|
||||
return "F" + setFeedrate.Value;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace OpenNest.IO
|
||||
@@ -15,6 +19,7 @@ namespace OpenNest.IO
|
||||
private CodeSection section;
|
||||
private Program program;
|
||||
private StreamReader reader;
|
||||
private Dictionary<string, double> resolvedVariables;
|
||||
|
||||
public ProgramReader(Stream stream)
|
||||
{
|
||||
@@ -24,11 +29,38 @@ namespace OpenNest.IO
|
||||
|
||||
public Program Read()
|
||||
{
|
||||
// First pass: read all lines, collect variable definitions
|
||||
var allLines = new List<string>();
|
||||
var variableDefs = new Dictionary<string, (string expression, bool inline, bool global)>(
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
var codeLines = new List<string>();
|
||||
string line;
|
||||
|
||||
while ((line = reader.ReadLine()) != null)
|
||||
{
|
||||
block = ParseBlock(line);
|
||||
allLines.Add(line);
|
||||
if (TryParseVariableDefinition(line, out var name, out var expression, out var isInline, out var isGlobal))
|
||||
variableDefs[name] = (expression, isInline, isGlobal);
|
||||
else
|
||||
codeLines.Add(line);
|
||||
}
|
||||
|
||||
// Evaluate variables with topological sort for dependency ordering
|
||||
resolvedVariables = ResolveVariables(variableDefs);
|
||||
|
||||
// Store evaluated variables on the program
|
||||
foreach (var kvp in variableDefs)
|
||||
{
|
||||
var name = kvp.Key;
|
||||
var (expression, isInline, isGlobal) = kvp.Value;
|
||||
var value = resolvedVariables[name];
|
||||
program.Variables[name] = new VariableDefinition(name, expression, value, isInline, isGlobal);
|
||||
}
|
||||
|
||||
// Second pass: parse G-code lines with variable substitution
|
||||
foreach (var codeLine in codeLines)
|
||||
{
|
||||
block = ParseBlock(codeLine);
|
||||
ProcessCurrentBlock();
|
||||
}
|
||||
|
||||
@@ -39,10 +71,43 @@ namespace OpenNest.IO
|
||||
{
|
||||
var block = new CodeBlock();
|
||||
Code code = null;
|
||||
for (int i = 0; i < line.Length; ++i)
|
||||
for (var i = 0; i < line.Length; ++i)
|
||||
{
|
||||
var c = line[i];
|
||||
if (char.IsLetter(c))
|
||||
if (c == '$' && code != null && resolvedVariables != null)
|
||||
{
|
||||
// Read the maximal variable name (letters, digits, underscores)
|
||||
var start = i + 1;
|
||||
while (start < line.Length && (char.IsLetterOrDigit(line[start]) || line[start] == '_'))
|
||||
start++;
|
||||
var maxName = line.Substring(i + 1, start - i - 1);
|
||||
|
||||
// Try longest match first, then progressively shorter to handle
|
||||
// cases like X$widthY0 where "widthY" isn't a variable but "width" is
|
||||
string lookupKey = null;
|
||||
var nameLen = maxName.Length;
|
||||
while (nameLen > 0)
|
||||
{
|
||||
var candidate = maxName.Substring(0, nameLen);
|
||||
lookupKey = resolvedVariables.Keys
|
||||
.FirstOrDefault(k => string.Equals(k, candidate, StringComparison.OrdinalIgnoreCase));
|
||||
if (lookupKey != null)
|
||||
break;
|
||||
nameLen--;
|
||||
}
|
||||
|
||||
if (lookupKey != null)
|
||||
{
|
||||
code.Value = resolvedVariables[lookupKey].ToString(CultureInfo.InvariantCulture);
|
||||
code.VariableRef = lookupKey;
|
||||
i += nameLen; // advance past the matched variable name
|
||||
}
|
||||
else
|
||||
{
|
||||
i = start - 1; // no match, skip the whole thing
|
||||
}
|
||||
}
|
||||
else if (char.IsLetter(c))
|
||||
block.Add((code = new Code(c)));
|
||||
else if (c == ':')
|
||||
{
|
||||
@@ -125,7 +190,10 @@ namespace OpenNest.IO
|
||||
break;
|
||||
|
||||
case 'F':
|
||||
program.Codes.Add(new Feedrate() { Value = double.Parse(code.Value) });
|
||||
var feedrate = new Feedrate() { Value = double.Parse(code.Value) };
|
||||
if (code.VariableRef != null)
|
||||
feedrate.VariableRef = code.VariableRef;
|
||||
program.Codes.Add(feedrate);
|
||||
code = GetNextCode();
|
||||
break;
|
||||
|
||||
@@ -143,6 +211,7 @@ namespace OpenNest.IO
|
||||
double y = 0;
|
||||
var layer = LayerType.Cut;
|
||||
var suppressed = false;
|
||||
string xRef = null, yRef = null;
|
||||
|
||||
while (section == CodeSection.Line)
|
||||
{
|
||||
@@ -157,10 +226,12 @@ namespace OpenNest.IO
|
||||
{
|
||||
case 'X':
|
||||
x = double.Parse(code.Value);
|
||||
xRef = code.VariableRef;
|
||||
break;
|
||||
|
||||
case 'Y':
|
||||
y = double.Parse(code.Value);
|
||||
yRef = code.VariableRef;
|
||||
break;
|
||||
|
||||
case ':':
|
||||
@@ -200,10 +271,13 @@ namespace OpenNest.IO
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var refs = BuildVariableRefs(("X", xRef), ("Y", yRef));
|
||||
|
||||
if (isRapid)
|
||||
program.Codes.Add(new RapidMove(x, y));
|
||||
program.Codes.Add(new RapidMove(x, y) { VariableRefs = refs });
|
||||
else
|
||||
program.Codes.Add(new LinearMove(x, y) { Layer = layer, Suppressed = suppressed });
|
||||
program.Codes.Add(new LinearMove(x, y) { Layer = layer, Suppressed = suppressed, VariableRefs = refs });
|
||||
}
|
||||
|
||||
private void ReadArc(RotationType rotation)
|
||||
@@ -214,6 +288,7 @@ namespace OpenNest.IO
|
||||
double j = 0;
|
||||
var layer = LayerType.Cut;
|
||||
var suppressed = false;
|
||||
string xRef = null, yRef = null, iRef = null, jRef = null;
|
||||
|
||||
while (section == CodeSection.Arc)
|
||||
{
|
||||
@@ -229,18 +304,22 @@ namespace OpenNest.IO
|
||||
{
|
||||
case 'X':
|
||||
x = double.Parse(code.Value);
|
||||
xRef = code.VariableRef;
|
||||
break;
|
||||
|
||||
case 'Y':
|
||||
y = double.Parse(code.Value);
|
||||
yRef = code.VariableRef;
|
||||
break;
|
||||
|
||||
case 'I':
|
||||
i = double.Parse(code.Value);
|
||||
iRef = code.VariableRef;
|
||||
break;
|
||||
|
||||
case 'J':
|
||||
j = double.Parse(code.Value);
|
||||
jRef = code.VariableRef;
|
||||
break;
|
||||
|
||||
case ':':
|
||||
@@ -286,7 +365,8 @@ namespace OpenNest.IO
|
||||
CenterPoint = new Vector(i, j),
|
||||
Rotation = rotation,
|
||||
Layer = layer,
|
||||
Suppressed = suppressed
|
||||
Suppressed = suppressed,
|
||||
VariableRefs = BuildVariableRefs(("X", xRef), ("Y", yRef), ("I", iRef), ("J", jRef))
|
||||
});
|
||||
}
|
||||
|
||||
@@ -351,6 +431,179 @@ namespace OpenNest.IO
|
||||
return block[codeIndex];
|
||||
}
|
||||
|
||||
private static bool TryParseVariableDefinition(string line, out string name, out string expression,
|
||||
out bool isInline, out bool isGlobal)
|
||||
{
|
||||
name = null;
|
||||
expression = null;
|
||||
isInline = false;
|
||||
isGlobal = false;
|
||||
|
||||
var trimmed = line.Trim();
|
||||
if (trimmed.Length == 0)
|
||||
return false;
|
||||
|
||||
// Must start with a letter or underscore (not a G-code letter followed by a digit)
|
||||
var firstChar = trimmed[0];
|
||||
if (!char.IsLetter(firstChar) && firstChar != '_')
|
||||
return false;
|
||||
|
||||
// If line starts with a known G-code letter followed by a digit, it's not a variable
|
||||
if (trimmed.Length >= 2 && char.IsDigit(trimmed[1]))
|
||||
{
|
||||
var upper = char.ToUpper(firstChar);
|
||||
if (upper is 'G' or 'M' or 'N' or 'F' or 'X' or 'Y' or 'I' or 'J' or 'T' or 'S' or 'O' or 'P' or 'R')
|
||||
return false;
|
||||
}
|
||||
|
||||
// Must contain '='
|
||||
var eqIndex = trimmed.IndexOf('=');
|
||||
if (eqIndex < 1)
|
||||
return false;
|
||||
|
||||
// Extract name (everything before '=', trimmed)
|
||||
var rawName = trimmed.Substring(0, eqIndex).Trim();
|
||||
|
||||
// Validate name: must be identifier (letter/underscore followed by alphanumeric/underscore)
|
||||
if (rawName.Length == 0 || (!char.IsLetter(rawName[0]) && rawName[0] != '_'))
|
||||
return false;
|
||||
for (var i = 1; i < rawName.Length; i++)
|
||||
{
|
||||
if (!char.IsLetterOrDigit(rawName[i]) && rawName[i] != '_')
|
||||
return false;
|
||||
}
|
||||
|
||||
// Extract expression and flags from the remainder after '='
|
||||
var remainder = trimmed.Substring(eqIndex + 1).Trim();
|
||||
|
||||
// Check for trailing flags: inline and/or global
|
||||
// Parse from the end to separate expression from flags
|
||||
var words = remainder.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
var flagStart = words.Length;
|
||||
for (var i = words.Length - 1; i >= 0; i--)
|
||||
{
|
||||
var word = words[i].ToLowerInvariant();
|
||||
if (word == "inline" || word == "global")
|
||||
flagStart = i;
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
// Build expression from non-flag words
|
||||
var expressionParts = words.Take(flagStart).ToArray();
|
||||
if (expressionParts.Length == 0)
|
||||
return false;
|
||||
|
||||
expression = string.Join(" ", expressionParts);
|
||||
|
||||
// Parse flags
|
||||
for (var i = flagStart; i < words.Length; i++)
|
||||
{
|
||||
var word = words[i].ToLowerInvariant();
|
||||
if (word == "inline") isInline = true;
|
||||
else if (word == "global") isGlobal = true;
|
||||
}
|
||||
|
||||
name = rawName;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static Dictionary<string, double> ResolveVariables(
|
||||
Dictionary<string, (string expression, bool inline, bool global)> variableDefs)
|
||||
{
|
||||
if (variableDefs.Count == 0)
|
||||
return new Dictionary<string, double>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// Build dependency graph
|
||||
var dependencies = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var kvp in variableDefs)
|
||||
{
|
||||
var deps = new List<string>();
|
||||
var expr = kvp.Value.expression;
|
||||
for (var i = 0; i < expr.Length; i++)
|
||||
{
|
||||
if (expr[i] == '$')
|
||||
{
|
||||
var start = i + 1;
|
||||
while (start < expr.Length && (char.IsLetterOrDigit(expr[start]) || expr[start] == '_'))
|
||||
start++;
|
||||
var refName = expr.Substring(i + 1, start - i - 1);
|
||||
// Find the canonical name (case-insensitive match)
|
||||
var canonical = variableDefs.Keys
|
||||
.FirstOrDefault(k => string.Equals(k, refName, StringComparison.OrdinalIgnoreCase));
|
||||
if (canonical != null)
|
||||
deps.Add(canonical);
|
||||
i = start - 1;
|
||||
}
|
||||
}
|
||||
dependencies[kvp.Key] = deps;
|
||||
}
|
||||
|
||||
// Topological sort (Kahn's algorithm)
|
||||
var inDegree = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var name in variableDefs.Keys)
|
||||
inDegree[name] = 0;
|
||||
foreach (var kvp in dependencies)
|
||||
{
|
||||
foreach (var dep in kvp.Value)
|
||||
{
|
||||
if (inDegree.ContainsKey(dep))
|
||||
inDegree[kvp.Key]++;
|
||||
}
|
||||
}
|
||||
|
||||
var queue = new Queue<string>();
|
||||
foreach (var kvp in inDegree)
|
||||
{
|
||||
if (kvp.Value == 0)
|
||||
queue.Enqueue(kvp.Key);
|
||||
}
|
||||
|
||||
var resolved = new Dictionary<string, double>(StringComparer.OrdinalIgnoreCase);
|
||||
var order = new List<string>();
|
||||
|
||||
while (queue.Count > 0)
|
||||
{
|
||||
var current = queue.Dequeue();
|
||||
order.Add(current);
|
||||
|
||||
// Evaluate this variable
|
||||
var expr = variableDefs[current].expression;
|
||||
var value = ExpressionEvaluator.Evaluate(expr, resolved);
|
||||
resolved[current] = value;
|
||||
|
||||
// Reduce in-degree of dependents
|
||||
foreach (var kvp in dependencies)
|
||||
{
|
||||
if (kvp.Value.Contains(current, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
inDegree[kvp.Key]--;
|
||||
if (inDegree[kvp.Key] == 0 && !order.Contains(kvp.Key))
|
||||
queue.Enqueue(kvp.Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (order.Count != variableDefs.Count)
|
||||
throw new InvalidOperationException("Circular dependency detected among variables.");
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> BuildVariableRefs(params (string axis, string varRef)[] refs)
|
||||
{
|
||||
Dictionary<string, string> result = null;
|
||||
foreach (var (axis, varRef) in refs)
|
||||
{
|
||||
if (varRef != null)
|
||||
{
|
||||
result ??= new Dictionary<string, string>();
|
||||
result[axis] = varRef;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
reader.Close();
|
||||
@@ -374,6 +627,8 @@ namespace OpenNest.IO
|
||||
|
||||
public string Value { get; set; }
|
||||
|
||||
public string VariableRef { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Id + Value;
|
||||
|
||||
@@ -29,6 +29,29 @@ public sealed class FeatureContext
|
||||
/// so part-relative programs become plate-absolute under G90.
|
||||
/// </summary>
|
||||
public Vector PartLocation { get; set; } = Vector.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// Maps (drawingId, variableName) to assigned machine variable numbers.
|
||||
/// Used to emit #number references instead of literal values for user variables.
|
||||
/// </summary>
|
||||
public Dictionary<(int drawingId, string varName), int> UserVariableMapping { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The drawing ID for the current part, used to look up user variable mappings.
|
||||
/// </summary>
|
||||
public int DrawingId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// True if this feature is a cut-off line. Used to substitute plate-edge
|
||||
/// coordinates with sheet width/length variables.
|
||||
/// </summary>
|
||||
public bool IsCutOff { get; set; }
|
||||
|
||||
/// <summary>Plate width (Y extent for vertical cutoffs).</summary>
|
||||
public double PlateWidth { get; set; }
|
||||
|
||||
/// <summary>Plate length (X extent for horizontal cutoffs).</summary>
|
||||
public double PlateLength { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -63,7 +86,7 @@ public sealed class CincinnatiFeatureWriter
|
||||
var piercePoint = FindPiercePoint(ctx.Codes);
|
||||
|
||||
// 1. Rapid to pierce point (with line number if configured)
|
||||
WriteRapidToPierce(writer, ctx.FeatureNumber, piercePoint, offset);
|
||||
WriteRapidToPierce(writer, ctx, piercePoint, offset);
|
||||
|
||||
// 2. Part name comment on first feature of each part
|
||||
if (ctx.IsFirstFeatureOfPart && !string.IsNullOrEmpty(ctx.PartName))
|
||||
@@ -112,7 +135,9 @@ public sealed class CincinnatiFeatureWriter
|
||||
kerfEmitted = true;
|
||||
}
|
||||
|
||||
sb.Append($"G1 X{_fmt.FormatCoord(linear.EndPoint.X + offset.X)} Y{_fmt.FormatCoord(linear.EndPoint.Y + offset.Y)}");
|
||||
var xCoord = FormatCoordWithVars(linear.EndPoint.X + offset.X, "X", linear.VariableRefs, ctx);
|
||||
var yCoord = FormatCoordWithVars(linear.EndPoint.Y + offset.Y, "Y", linear.VariableRefs, ctx);
|
||||
sb.Append($"G1 X{xCoord} Y{yCoord}");
|
||||
|
||||
// Feedrate — etch always uses process feedrate
|
||||
var feedVar = ctx.IsEtch ? "#148" : GetLinearFeedVariable(linear.Layer);
|
||||
@@ -138,7 +163,9 @@ public sealed class CincinnatiFeatureWriter
|
||||
|
||||
// G2 = CW, G3 = CCW
|
||||
var gCode = arc.Rotation == RotationType.CW ? "G2" : "G3";
|
||||
sb.Append($"{gCode} X{_fmt.FormatCoord(arc.EndPoint.X + offset.X)} Y{_fmt.FormatCoord(arc.EndPoint.Y + offset.Y)}");
|
||||
var xCoord = FormatCoordWithVars(arc.EndPoint.X + offset.X, "X", arc.VariableRefs, ctx);
|
||||
var yCoord = FormatCoordWithVars(arc.EndPoint.Y + offset.Y, "Y", arc.VariableRefs, ctx);
|
||||
sb.Append($"{gCode} X{xCoord} Y{yCoord}");
|
||||
|
||||
// Convert absolute center to incremental I/J
|
||||
var i = arc.CenterPoint.X - currentPos.X;
|
||||
@@ -177,6 +204,52 @@ public sealed class CincinnatiFeatureWriter
|
||||
WriteM47(writer, ctx);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formats a coordinate value, using a #number variable reference if the motion
|
||||
/// has a VariableRef for this axis and the variable is mapped (non-inline).
|
||||
/// For cut-off features, plate-edge coordinates are substituted with
|
||||
/// the sheet width/length variables.
|
||||
/// Inline variables fall through to literal formatting.
|
||||
/// </summary>
|
||||
private string FormatCoordWithVars(double value, string axis,
|
||||
Dictionary<string, string> variableRefs, FeatureContext ctx)
|
||||
{
|
||||
// User-defined variable references take priority
|
||||
if (variableRefs != null
|
||||
&& variableRefs.TryGetValue(axis, out var varName)
|
||||
&& ctx.UserVariableMapping != null
|
||||
&& ctx.UserVariableMapping.TryGetValue((ctx.DrawingId, varName), out var varNum))
|
||||
{
|
||||
return $"#{varNum}";
|
||||
}
|
||||
|
||||
// Cut-off plate-edge substitution
|
||||
if (ctx.IsCutOff)
|
||||
{
|
||||
var sheetVar = MatchCutOffSheetVariable(value, axis, ctx);
|
||||
if (sheetVar != null)
|
||||
return sheetVar;
|
||||
}
|
||||
|
||||
return _fmt.FormatCoord(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For cut-off coordinates, checks if the value matches a plate edge dimension
|
||||
/// and returns the sheet variable reference (e.g., "#110") if so.
|
||||
/// </summary>
|
||||
private string MatchCutOffSheetVariable(double value, string axis, FeatureContext ctx)
|
||||
{
|
||||
// Vertical cutoffs travel along Y — the Y endpoint at the plate edge = sheet width
|
||||
// Horizontal cutoffs travel along X — the X endpoint at the plate edge = sheet length
|
||||
if (axis == "Y" && Tolerance.IsEqualTo(value, ctx.PlateWidth))
|
||||
return $"#{_config.SheetWidthVariable}";
|
||||
if (axis == "X" && Tolerance.IsEqualTo(value, ctx.PlateLength))
|
||||
return $"#{_config.SheetLengthVariable}";
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private Vector FindPiercePoint(List<ICode> codes)
|
||||
{
|
||||
foreach (var code in codes)
|
||||
@@ -195,14 +268,16 @@ public sealed class CincinnatiFeatureWriter
|
||||
return Vector.Zero;
|
||||
}
|
||||
|
||||
private void WriteRapidToPierce(TextWriter writer, int featureNumber, Vector piercePoint, Vector offset)
|
||||
private void WriteRapidToPierce(TextWriter writer, FeatureContext ctx, Vector piercePoint, Vector offset)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
if (_config.UseLineNumbers)
|
||||
sb.Append($"N{featureNumber} ");
|
||||
sb.Append($"N{ctx.FeatureNumber} ");
|
||||
|
||||
sb.Append($"G0 X{_fmt.FormatCoord(piercePoint.X + offset.X)} Y{_fmt.FormatCoord(piercePoint.Y + offset.Y)}");
|
||||
var xCoord = FormatCoordWithVars(piercePoint.X + offset.X, "X", null, ctx);
|
||||
var yCoord = FormatCoordWithVars(piercePoint.Y + offset.Y, "Y", null, ctx);
|
||||
sb.Append($"G0 X{xCoord} Y{yCoord}");
|
||||
|
||||
writer.WriteLine(sb.ToString());
|
||||
}
|
||||
|
||||
@@ -66,7 +66,6 @@ public sealed class CincinnatiPartSubprogramWriter
|
||||
_featureWriter.Write(w, ctx);
|
||||
}
|
||||
|
||||
w.WriteLine("G0 X0 Y0");
|
||||
w.WriteLine($"M99 (END OF {drawingName})");
|
||||
}
|
||||
|
||||
|
||||
@@ -253,6 +253,11 @@ namespace OpenNest.Posts.Cincinnati
|
||||
new() { MaxRadius = 4.500, FeedratePercent = 0.80, VariableNumber = 125 }
|
||||
};
|
||||
|
||||
[Category("A. Variables")]
|
||||
[DisplayName("User Variable Start")]
|
||||
[Description("Starting variable number for user-defined variables (#200, #201, etc.).")]
|
||||
public int UserVariableStart { get; set; } = 200;
|
||||
|
||||
[Category("A. Variables")]
|
||||
[DisplayName("Sheet Width Variable")]
|
||||
[Description("Variable number for sheet width.")]
|
||||
|
||||
@@ -70,7 +70,10 @@ namespace OpenNest.Posts.Cincinnati
|
||||
.Where(p => p.Parts.Count > 0)
|
||||
.ToList();
|
||||
|
||||
// 3. Resolve gas and library files
|
||||
// 3. Register user variables from drawing programs
|
||||
var userVarMapping = RegisterUserVariables(vars, plates);
|
||||
|
||||
// 4. Resolve gas and library files
|
||||
var resolver = new MaterialLibraryResolver(Config);
|
||||
var gas = MaterialLibraryResolver.ResolveGas(nest, Config);
|
||||
var etchLibrary = resolver.ResolveEtchLibrary(Config.DefaultEtchGas);
|
||||
@@ -79,42 +82,41 @@ namespace OpenNest.Posts.Cincinnati
|
||||
var firstPlate = plates.FirstOrDefault();
|
||||
var initialCutLibrary = resolver.ResolveCutLibrary(nest.Material?.Name ?? "", nest.Thickness, gas);
|
||||
|
||||
// 4. Build part sub-program registry (if enabled)
|
||||
// 5. Build part sub-program registry (if enabled)
|
||||
Dictionary<(int, long), int> partSubprograms = null;
|
||||
List<(int subNum, string name, Program program)> subprogramEntries = null;
|
||||
|
||||
if (Config.UsePartSubprograms)
|
||||
(partSubprograms, subprogramEntries) = CincinnatiPartSubprogramWriter.BuildRegistry(plates, Config.PartSubprogramStart);
|
||||
|
||||
// 5. Create writers
|
||||
// 6. Create writers
|
||||
var preamble = new CincinnatiPreambleWriter(Config);
|
||||
var sheetWriter = new CincinnatiSheetWriter(Config, vars);
|
||||
|
||||
// 6. Build material description from nest
|
||||
// 7. Build material description from nest
|
||||
var material = nest.Material;
|
||||
var materialDesc = material != null
|
||||
? $"{material.Name}{(string.IsNullOrEmpty(material.Grade) ? "" : $", {material.Grade}")}"
|
||||
: "";
|
||||
|
||||
// 7. Write to stream
|
||||
// 8. Write to stream
|
||||
using var writer = new StreamWriter(outputStream, Encoding.UTF8, 1024, leaveOpen: true);
|
||||
|
||||
// Main program
|
||||
preamble.WriteMainProgram(writer, nest.Name ?? "NEST", materialDesc, plates.Count, initialCutLibrary);
|
||||
preamble.WriteMainProgram(writer, nest.Name ?? "NEST", materialDesc, plates, initialCutLibrary);
|
||||
|
||||
// Variable declaration subprogram
|
||||
preamble.WriteVariableDeclaration(writer, vars);
|
||||
|
||||
// Sheet subprograms
|
||||
// Sheet subprograms (one per unique layout, quantity handled via L count in main)
|
||||
for (var i = 0; i < plates.Count; i++)
|
||||
{
|
||||
var plate = plates[i];
|
||||
var sheetIndex = i + 1;
|
||||
var layoutIndex = i + 1;
|
||||
var subNumber = Config.SheetSubprogramStart + i;
|
||||
var cutLibrary = resolver.ResolveCutLibrary(nest.Material?.Name ?? "", nest.Thickness, gas);
|
||||
var isLastSheet = i == plates.Count - 1;
|
||||
sheetWriter.Write(writer, plate, nest.Name ?? "NEST", sheetIndex, subNumber,
|
||||
cutLibrary, etchLibrary, partSubprograms, isLastSheet);
|
||||
sheetWriter.Write(writer, plate, nest.Name ?? "NEST", layoutIndex, subNumber,
|
||||
cutLibrary, etchLibrary, partSubprograms, userVarMapping);
|
||||
}
|
||||
|
||||
// Part sub-programs (if enabled)
|
||||
@@ -142,6 +144,103 @@ namespace OpenNest.Posts.Cincinnati
|
||||
Post(nest, fs);
|
||||
}
|
||||
|
||||
private Dictionary<(int drawingId, string varName), int> RegisterUserVariables(
|
||||
ProgramVariableManager vars, List<Plate> plates)
|
||||
{
|
||||
var mapping = new Dictionary<(int drawingId, string varName), int>();
|
||||
var nextNumber = Config.UserVariableStart;
|
||||
|
||||
// Track global variables by name so they share a single number
|
||||
var globalNumbers = new Dictionary<string, int>(System.StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// Collect unique drawings from all plates
|
||||
var seenDrawings = new HashSet<int>();
|
||||
foreach (var plate in plates)
|
||||
{
|
||||
foreach (var part in plate.Parts)
|
||||
{
|
||||
var drawing = part.BaseDrawing;
|
||||
if (drawing.IsCutOff || !seenDrawings.Add(drawing.Id))
|
||||
continue;
|
||||
|
||||
foreach (var kvp in drawing.Program.Variables)
|
||||
{
|
||||
var varDef = kvp.Value;
|
||||
|
||||
// Skip inline variables — they emit literal values
|
||||
if (varDef.Inline)
|
||||
continue;
|
||||
|
||||
if (varDef.Global)
|
||||
{
|
||||
if (!globalNumbers.TryGetValue(varDef.Name, out var globalNum))
|
||||
{
|
||||
globalNum = nextNumber++;
|
||||
globalNumbers[varDef.Name] = globalNum;
|
||||
|
||||
// Register once in the variable manager
|
||||
var commentName = ToPascalCase(varDef.Name);
|
||||
var expression = FormatVariableValue(varDef.Value);
|
||||
vars.GetOrCreate(commentName, globalNum, expression);
|
||||
}
|
||||
|
||||
mapping[(drawing.Id, varDef.Name)] = globalNum;
|
||||
}
|
||||
else
|
||||
{
|
||||
var num = nextNumber++;
|
||||
mapping[(drawing.Id, varDef.Name)] = num;
|
||||
|
||||
// Register with drawing name prefix in the comment
|
||||
var drawingLabel = ToPascalCase(drawing.Name);
|
||||
var varLabel = ToPascalCase(varDef.Name);
|
||||
var commentName = $"{drawingLabel}{varLabel}";
|
||||
var expression = FormatVariableValue(varDef.Value);
|
||||
vars.GetOrCreate(commentName, num, expression);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mapping;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a variable name from snake_case or camelCase to PascalCase.
|
||||
/// Examples: "sheet_width" → "SheetWidth", "holeSpacing" → "HoleSpacing"
|
||||
/// </summary>
|
||||
private static string ToPascalCase(string name)
|
||||
{
|
||||
var sb = new StringBuilder(name.Length);
|
||||
var capitalizeNext = true;
|
||||
|
||||
foreach (var c in name)
|
||||
{
|
||||
if (c == '_')
|
||||
{
|
||||
capitalizeNext = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (capitalizeNext)
|
||||
{
|
||||
sb.Append(char.ToUpper(c));
|
||||
capitalizeNext = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append(c);
|
||||
}
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string FormatVariableValue(double value)
|
||||
{
|
||||
return value.ToString("0.####", System.Globalization.CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
private ProgramVariableManager CreateVariableManager()
|
||||
{
|
||||
var vars = new ProgramVariableManager();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using OpenNest;
|
||||
using OpenNest.CNC;
|
||||
@@ -23,7 +24,7 @@ public sealed class CincinnatiPreambleWriter
|
||||
/// </summary>
|
||||
/// <param name="initialLibrary">Resolved G89 library file for the initial process setup.</param>
|
||||
public void WriteMainProgram(TextWriter w, string nestName, string materialDescription,
|
||||
int sheetCount, string initialLibrary)
|
||||
List<Plate> plates, string initialLibrary)
|
||||
{
|
||||
w.WriteLine(CoordinateFormatter.Comment($"NEST {nestName}"));
|
||||
w.WriteLine(CoordinateFormatter.Comment($"CONFIGURATION - {_config.ConfigurationName}"));
|
||||
@@ -54,10 +55,16 @@ public sealed class CincinnatiPreambleWriter
|
||||
|
||||
w.WriteLine("GOTO1 (GOTO SHEET NUMBER)");
|
||||
|
||||
for (var i = 1; i <= sheetCount; i++)
|
||||
for (var i = 0; i < plates.Count; i++)
|
||||
{
|
||||
var subNum = _config.SheetSubprogramStart + (i - 1);
|
||||
w.WriteLine($"N{i} M98 P{subNum} (SHEET {i})");
|
||||
var layoutNumber = i + 1;
|
||||
var subNum = _config.SheetSubprogramStart + i;
|
||||
var qty = System.Math.Max(plates[i].Quantity, 1);
|
||||
var lParam = qty > 1 ? $" L{qty}" : "";
|
||||
var sheetLabel = qty > 1
|
||||
? $"LAYOUT {layoutNumber} - {qty} SHEETS"
|
||||
: $"LAYOUT {layoutNumber}";
|
||||
w.WriteLine($"N{layoutNumber} M98 P{subNum}{lParam} ({sheetLabel})");
|
||||
}
|
||||
|
||||
w.WriteLine("M42");
|
||||
|
||||
@@ -35,10 +35,10 @@ public sealed class CincinnatiSheetWriter
|
||||
/// Optional mapping of (drawingId, rotationKey) to sub-program number.
|
||||
/// When provided, non-cutoff parts are emitted as M98 calls instead of inline features.
|
||||
/// </param>
|
||||
public void Write(TextWriter w, Plate plate, string nestName, int sheetIndex, int subNumber,
|
||||
public void Write(TextWriter w, Plate plate, string nestName, int layoutIndex, int subNumber,
|
||||
string cutLibrary, string etchLibrary,
|
||||
Dictionary<(int, long), int> partSubprograms = null,
|
||||
bool isLastSheet = false)
|
||||
Dictionary<(int drawingId, string varName), int> userVarMapping = null)
|
||||
{
|
||||
if (plate.Parts.Count == 0)
|
||||
return;
|
||||
@@ -51,11 +51,10 @@ public sealed class CincinnatiSheetWriter
|
||||
|
||||
// 1. Sheet header
|
||||
w.WriteLine("(*****************************************************)");
|
||||
w.WriteLine($"( START OF {nestName}.{sheetIndex:D3} )");
|
||||
w.WriteLine($"( START OF {nestName}.{layoutIndex:D3} )");
|
||||
w.WriteLine($":{subNumber}");
|
||||
w.WriteLine($"( Sheet {sheetIndex} )");
|
||||
w.WriteLine($"( Layout {sheetIndex} )");
|
||||
w.WriteLine($"( SHEET NAME = {_fmt.FormatCoord(length)} X {_fmt.FormatCoord(width)} )");
|
||||
w.WriteLine($"( Layout {layoutIndex} )");
|
||||
w.WriteLine($"( SHEET NAME = {_fmt.FormatCoord(width)} X {_fmt.FormatCoord(length)} )");
|
||||
w.WriteLine($"( Total parts on sheet = {partCount} )");
|
||||
w.WriteLine($"#{_config.SheetWidthVariable}={_fmt.FormatCoord(width)} (SHEET WIDTH FOR CUTOFFS)");
|
||||
w.WriteLine($"#{_config.SheetLengthVariable}={_fmt.FormatCoord(length)} (SHEET LENGTH FOR CUTOFFS)");
|
||||
@@ -88,23 +87,22 @@ public sealed class CincinnatiSheetWriter
|
||||
|
||||
// 4. Emit parts
|
||||
if (partSubprograms != null)
|
||||
WritePartsWithSubprograms(w, allParts, cutLibrary, etchLibrary, sheetDiagonal, partSubprograms);
|
||||
WritePartsWithSubprograms(w, allParts, cutLibrary, etchLibrary, sheetDiagonal, width, length, partSubprograms, userVarMapping);
|
||||
else
|
||||
WritePartsInline(w, allParts, cutLibrary, etchLibrary, sheetDiagonal);
|
||||
WritePartsInline(w, allParts, cutLibrary, etchLibrary, sheetDiagonal, width, length, userVarMapping);
|
||||
|
||||
// 5. Footer
|
||||
w.WriteLine("M42");
|
||||
w.WriteLine("G0 X0 Y0");
|
||||
var emitM50 = _config.PalletExchange == PalletMode.EndOfSheet
|
||||
|| (_config.PalletExchange == PalletMode.StartAndEnd && isLastSheet);
|
||||
if (emitM50)
|
||||
w.WriteLine($"N{sheetIndex + 1} M50");
|
||||
w.WriteLine($"M99 (END OF {nestName}.{sheetIndex:D3})");
|
||||
if (_config.PalletExchange != PalletMode.None)
|
||||
w.WriteLine("M50");
|
||||
w.WriteLine($"M99 (END OF {nestName}.{layoutIndex:D3})");
|
||||
}
|
||||
|
||||
private void WritePartsWithSubprograms(TextWriter w, List<Part> allParts,
|
||||
string cutLibrary, string etchLibrary, double sheetDiagonal,
|
||||
Dictionary<(int, long), int> partSubprograms)
|
||||
double plateWidth, double plateLength,
|
||||
Dictionary<(int, long), int> partSubprograms,
|
||||
Dictionary<(int drawingId, string varName), int> userVarMapping)
|
||||
{
|
||||
var lastPartName = "";
|
||||
var featureIndex = 0;
|
||||
@@ -154,7 +152,12 @@ public sealed class CincinnatiSheetWriter
|
||||
LibraryFile = isEtch ? etchLibrary : cutLibrary,
|
||||
CutDistance = cutDistance,
|
||||
SheetDiagonal = sheetDiagonal,
|
||||
PartLocation = part.Location
|
||||
PartLocation = part.Location,
|
||||
UserVariableMapping = userVarMapping,
|
||||
DrawingId = part.BaseDrawing.Id,
|
||||
IsCutOff = part.BaseDrawing.IsCutOff,
|
||||
PlateWidth = plateWidth,
|
||||
PlateLength = plateLength
|
||||
};
|
||||
|
||||
_featureWriter.Write(w, ctx);
|
||||
@@ -202,7 +205,9 @@ public sealed class CincinnatiSheetWriter
|
||||
}
|
||||
|
||||
private void WritePartsInline(TextWriter w, List<Part> allParts,
|
||||
string cutLibrary, string etchLibrary, double sheetDiagonal)
|
||||
string cutLibrary, string etchLibrary, double sheetDiagonal,
|
||||
double plateWidth, double plateLength,
|
||||
Dictionary<(int drawingId, string varName), int> userVarMapping)
|
||||
{
|
||||
// Split and classify features, ordering etch before cut per part
|
||||
var features = new List<(Part part, List<ICode> codes, bool isEtch)>();
|
||||
@@ -242,7 +247,12 @@ public sealed class CincinnatiSheetWriter
|
||||
LibraryFile = isEtch ? etchLibrary : cutLibrary,
|
||||
CutDistance = cutDistance,
|
||||
SheetDiagonal = sheetDiagonal,
|
||||
PartLocation = part.Location
|
||||
PartLocation = part.Location,
|
||||
UserVariableMapping = userVarMapping,
|
||||
DrawingId = part.BaseDrawing.Id,
|
||||
IsCutOff = part.BaseDrawing.IsCutOff,
|
||||
PlateWidth = plateWidth,
|
||||
PlateLength = plateLength
|
||||
};
|
||||
|
||||
_featureWriter.Write(w, ctx);
|
||||
|
||||
@@ -11,6 +11,6 @@
|
||||
<PostsDir>..\OpenNest\bin\$(Configuration)\$(TargetFramework)\Posts\</PostsDir>
|
||||
</PropertyGroup>
|
||||
<MakeDir Directories="$(PostsDir)" />
|
||||
<Copy SourceFiles="$(TargetPath)" DestinationFolder="$(PostsDir)" SkipUnchangedFiles="true" />
|
||||
<Copy SourceFiles="$(TargetPath)" DestinationFolder="$(PostsDir)" SkipUnchangedFiles="true" ContinueOnError="true" />
|
||||
</Target>
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Tests.CNC;
|
||||
|
||||
public class ProgramVariableTests
|
||||
{
|
||||
[Fact]
|
||||
public void LinearMove_VariableRefs_NullByDefault()
|
||||
{
|
||||
var move = new LinearMove(1.0, 2.0);
|
||||
Assert.Null(move.VariableRefs);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LinearMove_Clone_CopiesVariableRefs()
|
||||
{
|
||||
var move = new LinearMove(1.0, 2.0);
|
||||
move.VariableRefs = new Dictionary<string, string> { { "X", "width" } };
|
||||
var clone = (LinearMove)move.Clone();
|
||||
Assert.NotSame(move.VariableRefs, clone.VariableRefs);
|
||||
Assert.Equal("width", clone.VariableRefs["X"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LinearMove_Clone_NullRefs_StaysNull()
|
||||
{
|
||||
var move = new LinearMove(1.0, 2.0);
|
||||
var clone = (LinearMove)move.Clone();
|
||||
Assert.Null(clone.VariableRefs);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArcMove_Clone_CopiesVariableRefs()
|
||||
{
|
||||
var move = new ArcMove(1, 0, 0.5, 0);
|
||||
move.VariableRefs = new Dictionary<string, string> { { "I", "radius" } };
|
||||
var clone = (ArcMove)move.Clone();
|
||||
Assert.NotSame(move.VariableRefs, clone.VariableRefs);
|
||||
Assert.Equal("radius", clone.VariableRefs["I"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RapidMove_Clone_CopiesVariableRefs()
|
||||
{
|
||||
var move = new RapidMove(5.0, 0);
|
||||
move.VariableRefs = new Dictionary<string, string> { { "X", "start_x" } };
|
||||
var clone = (RapidMove)move.Clone();
|
||||
Assert.NotSame(move.VariableRefs, clone.VariableRefs);
|
||||
Assert.Equal("start_x", clone.VariableRefs["X"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Feedrate_VariableRef_NullByDefault()
|
||||
{
|
||||
var f = new Feedrate(100.0);
|
||||
Assert.Null(f.VariableRef);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Feedrate_Clone_CopiesVariableRef()
|
||||
{
|
||||
var f = new Feedrate(100.0) { VariableRef = "cut_speed" };
|
||||
var clone = (Feedrate)f.Clone();
|
||||
Assert.Equal("cut_speed", clone.VariableRef);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Motion_Rotate_ClearsVariableRefs()
|
||||
{
|
||||
var move = new LinearMove(1.0, 0);
|
||||
move.VariableRefs = new Dictionary<string, string> { { "X", "width" } };
|
||||
move.Rotate(System.Math.PI / 2);
|
||||
Assert.Null(move.VariableRefs);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Motion_Offset_ClearsVariableRefs()
|
||||
{
|
||||
var move = new LinearMove(1.0, 0);
|
||||
move.VariableRefs = new Dictionary<string, string> { { "X", "width" } };
|
||||
move.Offset(5.0, 0);
|
||||
Assert.Null(move.VariableRefs);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArcMove_Rotate_ClearsVariableRefs()
|
||||
{
|
||||
var move = new ArcMove(1, 0, 0.5, 0);
|
||||
move.VariableRefs = new Dictionary<string, string> { { "I", "radius" } };
|
||||
move.Rotate(System.Math.PI / 2);
|
||||
Assert.Null(move.VariableRefs);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArcMove_Offset_ClearsVariableRefs()
|
||||
{
|
||||
var move = new ArcMove(1, 0, 0.5, 0);
|
||||
move.VariableRefs = new Dictionary<string, string> { { "I", "radius" } };
|
||||
move.Offset(5.0, 0);
|
||||
Assert.Null(move.VariableRefs);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Program_Variables_EmptyByDefault()
|
||||
{
|
||||
var pgm = new Program();
|
||||
Assert.Empty(pgm.Variables);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Program_Variables_CaseInsensitive()
|
||||
{
|
||||
var pgm = new Program();
|
||||
pgm.Variables["Diameter"] = new VariableDefinition("Diameter", "0.3", 0.3);
|
||||
Assert.True(pgm.Variables.ContainsKey("diameter"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Program_Clone_DeepCopiesVariables()
|
||||
{
|
||||
var pgm = new Program();
|
||||
pgm.Variables["diameter"] = new VariableDefinition("diameter", "0.3", 0.3);
|
||||
pgm.Codes.Add(new LinearMove(1.0, 0));
|
||||
|
||||
var clone = (Program)pgm.Clone();
|
||||
|
||||
Assert.Single(clone.Variables);
|
||||
Assert.Equal(0.3, clone.Variables["diameter"].Value);
|
||||
// Verify it's a separate dictionary
|
||||
clone.Variables.Remove("diameter");
|
||||
Assert.Single(pgm.Variables);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using OpenNest.CNC;
|
||||
|
||||
namespace OpenNest.Tests.CNC;
|
||||
|
||||
public class VariableDefinitionTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_SetsAllProperties()
|
||||
{
|
||||
var v = new VariableDefinition("diameter", "0.3", 0.3);
|
||||
Assert.Equal("diameter", v.Name);
|
||||
Assert.Equal("0.3", v.Expression);
|
||||
Assert.Equal(0.3, v.Value);
|
||||
Assert.False(v.Inline);
|
||||
Assert.False(v.Global);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithFlags_SetsFlags()
|
||||
{
|
||||
var v = new VariableDefinition("width", "48.0", 48.0, inline: true, global: true);
|
||||
Assert.True(v.Inline);
|
||||
Assert.True(v.Global);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultFlags_AreFalse()
|
||||
{
|
||||
var v = new VariableDefinition("x", "1", 1.0);
|
||||
Assert.False(v.Inline);
|
||||
Assert.False(v.Global);
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@ public class CincinnatiPostProcessorTests
|
||||
|
||||
// Sheet subprogram
|
||||
Assert.Contains(":101", output);
|
||||
Assert.Contains("( Sheet 1 )", output);
|
||||
Assert.Contains("( Layout 1 )", output);
|
||||
Assert.Contains("G84", output);
|
||||
Assert.Contains("M99", output);
|
||||
}
|
||||
@@ -150,8 +150,8 @@ public class CincinnatiPostProcessorTests
|
||||
var output = Encoding.UTF8.GetString(ms.ToArray());
|
||||
|
||||
// Should only have one sheet subprogram call in main
|
||||
Assert.Contains("N1 M98 P101 (SHEET 1)", output);
|
||||
Assert.DoesNotContain("SHEET 2", output);
|
||||
Assert.Contains("N1 M98 P101 (LAYOUT 1)", output);
|
||||
Assert.DoesNotContain("LAYOUT 2", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -258,8 +258,7 @@ public class CincinnatiPostProcessorTests
|
||||
Assert.Contains(":200", output);
|
||||
Assert.Contains("G84", output);
|
||||
|
||||
// Sub-program ends with G0 X0 Y0 and M99
|
||||
Assert.Contains("G0 X0 Y0", output);
|
||||
// Sub-program ends with M99
|
||||
Assert.Contains("M99 (END OF Square)", output);
|
||||
|
||||
// G92 restore after M98 call
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using OpenNest.CNC;
|
||||
@@ -19,7 +20,8 @@ public class CincinnatiPreambleWriterTests
|
||||
using var sw = new StringWriter(sb);
|
||||
var writer = new CincinnatiPreambleWriter(config);
|
||||
|
||||
writer.WriteMainProgram(sw, "TestNest", "Mild Steel, 10GA", 2, "MS135N2PANEL.lib");
|
||||
var plates = new List<Plate> { new(48, 96), new(48, 96) };
|
||||
writer.WriteMainProgram(sw, "TestNest", "Mild Steel, 10GA", plates, "MS135N2PANEL.lib");
|
||||
|
||||
var output = sb.ToString();
|
||||
Assert.Contains("( NEST TestNest )", output);
|
||||
@@ -29,8 +31,8 @@ public class CincinnatiPreambleWriterTests
|
||||
Assert.Contains("G89 PMS135N2PANEL.lib", output);
|
||||
Assert.Contains("M98 P100 (Variable Declaration)", output);
|
||||
Assert.Contains("GOTO1 (GOTO SHEET NUMBER)", output);
|
||||
Assert.Contains("N1 M98 P101 (SHEET 1)", output);
|
||||
Assert.Contains("N2 M98 P102 (SHEET 2)", output);
|
||||
Assert.Contains("N1 M98 P101 (LAYOUT 1)", output);
|
||||
Assert.Contains("N2 M98 P102 (LAYOUT 2)", output);
|
||||
Assert.Contains("M30 (END OF MAIN)", output);
|
||||
}
|
||||
|
||||
@@ -42,7 +44,7 @@ public class CincinnatiPreambleWriterTests
|
||||
using var sw = new StringWriter(sb);
|
||||
var writer = new CincinnatiPreambleWriter(config);
|
||||
|
||||
writer.WriteMainProgram(sw, "Test", "", 1, "");
|
||||
writer.WriteMainProgram(sw, "Test", "", new List<Plate> { new(48, 96) }, "");
|
||||
|
||||
Assert.Contains("G21 G90", sb.ToString());
|
||||
}
|
||||
@@ -55,7 +57,7 @@ public class CincinnatiPreambleWriterTests
|
||||
using var sw = new StringWriter(sb);
|
||||
var writer = new CincinnatiPreambleWriter(config);
|
||||
|
||||
writer.WriteMainProgram(sw, "Test", "", 1, "");
|
||||
writer.WriteMainProgram(sw, "Test", "", new List<Plate> { new(48, 96) }, "");
|
||||
|
||||
Assert.Contains("G20 G90", sb.ToString());
|
||||
}
|
||||
@@ -68,7 +70,7 @@ public class CincinnatiPreambleWriterTests
|
||||
using var sw = new StringWriter(sb);
|
||||
var writer = new CincinnatiPreambleWriter(config);
|
||||
|
||||
writer.WriteMainProgram(sw, "Test", "", 1, "");
|
||||
writer.WriteMainProgram(sw, "Test", "", new List<Plate> { new(48, 96) }, "");
|
||||
|
||||
Assert.Contains("G121 (SMART RAPIDS)", sb.ToString());
|
||||
}
|
||||
@@ -81,7 +83,7 @@ public class CincinnatiPreambleWriterTests
|
||||
using var sw = new StringWriter(sb);
|
||||
var writer = new CincinnatiPreambleWriter(config);
|
||||
|
||||
writer.WriteMainProgram(sw, "Test", "", 1, "");
|
||||
writer.WriteMainProgram(sw, "Test", "", new List<Plate> { new(48, 96) }, "");
|
||||
|
||||
Assert.DoesNotContain("G121", sb.ToString());
|
||||
}
|
||||
@@ -94,7 +96,7 @@ public class CincinnatiPreambleWriterTests
|
||||
using var sw = new StringWriter(sb);
|
||||
var writer = new CincinnatiPreambleWriter(config);
|
||||
|
||||
writer.WriteMainProgram(sw, "Test", "", 1, "");
|
||||
writer.WriteMainProgram(sw, "Test", "", new List<Plate> { new(48, 96) }, "");
|
||||
|
||||
Assert.Contains("M50", sb.ToString());
|
||||
}
|
||||
@@ -107,7 +109,7 @@ public class CincinnatiPreambleWriterTests
|
||||
using var sw = new StringWriter(sb);
|
||||
var writer = new CincinnatiPreambleWriter(config);
|
||||
|
||||
writer.WriteMainProgram(sw, "Test", "", 1, "");
|
||||
writer.WriteMainProgram(sw, "Test", "", new List<Plate> { new(48, 96) }, "");
|
||||
|
||||
Assert.DoesNotContain("M50", sb.ToString());
|
||||
}
|
||||
@@ -120,7 +122,7 @@ public class CincinnatiPreambleWriterTests
|
||||
using var sw = new StringWriter(sb);
|
||||
var writer = new CincinnatiPreambleWriter(config);
|
||||
|
||||
writer.WriteMainProgram(sw, "Test", "", 1, "");
|
||||
writer.WriteMainProgram(sw, "Test", "", new List<Plate> { new(48, 96) }, "");
|
||||
|
||||
Assert.Contains("G61", sb.ToString());
|
||||
}
|
||||
@@ -133,11 +135,33 @@ public class CincinnatiPreambleWriterTests
|
||||
using var sw = new StringWriter(sb);
|
||||
var writer = new CincinnatiPreambleWriter(config);
|
||||
|
||||
writer.WriteMainProgram(sw, "Test", "", 1, "");
|
||||
writer.WriteMainProgram(sw, "Test", "", new List<Plate> { new(48, 96) }, "");
|
||||
|
||||
Assert.DoesNotContain("G61", sb.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteMainProgram_EmitsLCount_WhenQuantityGreaterThanOne()
|
||||
{
|
||||
var config = new CincinnatiPostConfig { PostedUnits = Units.Inches };
|
||||
var sb = new StringBuilder();
|
||||
using var sw = new StringWriter(sb);
|
||||
var writer = new CincinnatiPreambleWriter(config);
|
||||
|
||||
var plates = new List<Plate>
|
||||
{
|
||||
new(48, 96) { Quantity = 5 },
|
||||
new(72, 48) { Quantity = 2 },
|
||||
new(36, 48) { Quantity = 1 }
|
||||
};
|
||||
writer.WriteMainProgram(sw, "Test", "", plates, "");
|
||||
|
||||
var output = sb.ToString();
|
||||
Assert.Contains("N1 M98 P101 L5 (LAYOUT 1 - 5 SHEETS)", output);
|
||||
Assert.Contains("N2 M98 P102 L2 (LAYOUT 2 - 2 SHEETS)", output);
|
||||
Assert.Contains("N3 M98 P103 (LAYOUT 3)", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteVariableDeclaration_EmitsSubprogram()
|
||||
{
|
||||
|
||||
@@ -28,7 +28,7 @@ public class CincinnatiSheetWriterTests
|
||||
|
||||
var output = sb.ToString();
|
||||
Assert.Contains(":101", output);
|
||||
Assert.Contains("( Sheet 1 )", output);
|
||||
Assert.Contains("( Layout 1 )", output);
|
||||
Assert.Contains("#110=", output);
|
||||
Assert.Contains("#111=", output);
|
||||
Assert.Contains("G92 X#5021 Y#5022", output);
|
||||
@@ -55,7 +55,6 @@ public class CincinnatiSheetWriterTests
|
||||
|
||||
var output = sb.ToString();
|
||||
Assert.Contains("M42", output);
|
||||
Assert.Contains("G0 X0 Y0", output);
|
||||
Assert.Contains("M50", output);
|
||||
}
|
||||
|
||||
@@ -143,7 +142,7 @@ public class CincinnatiSheetWriterTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteSheet_StartAndEnd_NoM50OnNonLastSheet()
|
||||
public void WriteSheet_StartAndEnd_EmitsM50()
|
||||
{
|
||||
var config = new CincinnatiPostConfig
|
||||
{
|
||||
@@ -157,33 +156,33 @@ public class CincinnatiSheetWriterTests
|
||||
using var sw = new StringWriter(sb);
|
||||
var sheetWriter = new CincinnatiSheetWriter(config, new ProgramVariableManager());
|
||||
|
||||
sheetWriter.Write(sw, plate, "TestNest", 1, 101, "", "", isLastSheet: false);
|
||||
|
||||
var output = sb.ToString();
|
||||
Assert.DoesNotContain("M50", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteSheet_StartAndEnd_M50OnLastSheet()
|
||||
{
|
||||
var config = new CincinnatiPostConfig
|
||||
{
|
||||
PalletExchange = PalletMode.StartAndEnd,
|
||||
PostedAccuracy = 4
|
||||
};
|
||||
var plate = new Plate(48.0, 96.0);
|
||||
plate.Parts.Add(new Part(new Drawing("TestPart", CreateSimpleProgram())));
|
||||
|
||||
var sb = new StringBuilder();
|
||||
using var sw = new StringWriter(sb);
|
||||
var sheetWriter = new CincinnatiSheetWriter(config, new ProgramVariableManager());
|
||||
|
||||
sheetWriter.Write(sw, plate, "TestNest", 1, 101, "", "", isLastSheet: true);
|
||||
sheetWriter.Write(sw, plate, "TestNest", 1, 101, "", "");
|
||||
|
||||
var output = sb.ToString();
|
||||
Assert.Contains("M50", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteSheet_NoPalletExchange_OmitsM50()
|
||||
{
|
||||
var config = new CincinnatiPostConfig
|
||||
{
|
||||
PalletExchange = PalletMode.None,
|
||||
PostedAccuracy = 4
|
||||
};
|
||||
var plate = new Plate(48.0, 96.0);
|
||||
plate.Parts.Add(new Part(new Drawing("TestPart", CreateSimpleProgram())));
|
||||
|
||||
var sb = new StringBuilder();
|
||||
using var sw = new StringWriter(sb);
|
||||
var sheetWriter = new CincinnatiSheetWriter(config, new ProgramVariableManager());
|
||||
|
||||
sheetWriter.Write(sw, plate, "TestNest", 1, 101, "", "");
|
||||
|
||||
var output = sb.ToString();
|
||||
Assert.DoesNotContain("M50", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteSheet_EndOfSheet_AlwaysEmitsM50()
|
||||
{
|
||||
@@ -199,7 +198,7 @@ public class CincinnatiSheetWriterTests
|
||||
using var sw = new StringWriter(sb);
|
||||
var sheetWriter = new CincinnatiSheetWriter(config, new ProgramVariableManager());
|
||||
|
||||
sheetWriter.Write(sw, plate, "TestNest", 1, 101, "", "", isLastSheet: false);
|
||||
sheetWriter.Write(sw, plate, "TestNest", 1, 101, "", "");
|
||||
|
||||
var output = sb.ToString();
|
||||
Assert.Contains("M50", output);
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.IO;
|
||||
using OpenNest.Posts.Cincinnati;
|
||||
|
||||
namespace OpenNest.Tests.Cincinnati;
|
||||
|
||||
public class UserVariablePostTests
|
||||
{
|
||||
[Fact]
|
||||
public void UserVariables_EmittedInDeclarationSubprogram()
|
||||
{
|
||||
var output = PostNestWithVariables("width = 48.0\nG90\nG01X$widthY0");
|
||||
|
||||
Assert.Contains("#200=48", output);
|
||||
Assert.Contains("WIDTH", output.ToUpper());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UserVariables_InlineVariable_NotEmittedAsNumbered()
|
||||
{
|
||||
var output = PostNestWithVariables("kerf = 0.06 inline\nG90\nG01X1Y0");
|
||||
|
||||
Assert.DoesNotContain("#200", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UserVariables_CoordinateUsesNumberedVariable()
|
||||
{
|
||||
var output = PostNestWithVariables("width = 48.0\nG90\nG01X$widthY0");
|
||||
|
||||
Assert.Contains("X#200", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UserVariables_InlineVariable_CoordinateUsesLiteral()
|
||||
{
|
||||
var output = PostNestWithVariables("kerf = 0.06 inline\nG90\nG01X$kerfY0");
|
||||
|
||||
Assert.Contains("X0.06", output);
|
||||
// G1 coordinate lines should not use X#nnn variable references for inline vars
|
||||
var g1Lines = output.Split('\n').Where(l => l.TrimStart().StartsWith("G1 ")).ToList();
|
||||
Assert.All(g1Lines, line => Assert.DoesNotContain("X#", line));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UserVariables_GlobalVariables_SharedAcrossDrawings()
|
||||
{
|
||||
var pgm1 = ParseProgram("sheet_width = 48.0 global\nG90\nG01X$sheet_widthY0");
|
||||
var pgm2 = ParseProgram("sheet_width = 48.0 global\nG90\nG01X$sheet_widthY0");
|
||||
|
||||
var drawing1 = new Drawing("Part1", pgm1);
|
||||
var drawing2 = new Drawing("Part2", pgm2);
|
||||
var nest = new Nest { Name = "Test" };
|
||||
nest.Drawings.Add(drawing1);
|
||||
nest.Drawings.Add(drawing2);
|
||||
var plate = new Plate(new Size(100, 100));
|
||||
plate.Parts.Add(new Part(drawing1, new Vector(0, 0)));
|
||||
plate.Parts.Add(new Part(drawing2, new Vector(50, 0)));
|
||||
nest.Plates.Add(plate);
|
||||
|
||||
var config = new CincinnatiPostConfig { UserVariableStart = 200 };
|
||||
var post = new CincinnatiPostProcessor(config);
|
||||
var output = PostToString(post, nest);
|
||||
|
||||
// Both should use the same #200 — only one declaration
|
||||
var declarationCount = output.Split('\n')
|
||||
.Count(l => l.Contains("#200=") && l.ToUpper().Contains("SHEET WIDTH"));
|
||||
Assert.Equal(1, declarationCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UserVariables_LocalVariables_GetSeparateNumbers()
|
||||
{
|
||||
var pgm1 = ParseProgram("diameter = 0.3\nG90\nG01X$diameterY0");
|
||||
var pgm2 = ParseProgram("diameter = 0.5\nG90\nG01X$diameterY0");
|
||||
|
||||
var drawing1 = new Drawing("TubeA", pgm1);
|
||||
var drawing2 = new Drawing("TubeB", pgm2);
|
||||
var nest = new Nest { Name = "Test" };
|
||||
nest.Drawings.Add(drawing1);
|
||||
nest.Drawings.Add(drawing2);
|
||||
var plate = new Plate(new Size(100, 100));
|
||||
plate.Parts.Add(new Part(drawing1, new Vector(0, 0)));
|
||||
plate.Parts.Add(new Part(drawing2, new Vector(50, 0)));
|
||||
nest.Plates.Add(plate);
|
||||
|
||||
var config = new CincinnatiPostConfig { UserVariableStart = 200 };
|
||||
var post = new CincinnatiPostProcessor(config);
|
||||
var output = PostToString(post, nest);
|
||||
|
||||
// Two separate declarations with different numbers
|
||||
Assert.Contains("#200=0.3", output);
|
||||
Assert.Contains("#201=0.5", output);
|
||||
Assert.Contains("TUBE A", output.ToUpper());
|
||||
Assert.Contains("TUBE B", output.ToUpper());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UserVariables_StartNumberConfigurable()
|
||||
{
|
||||
var config = new CincinnatiPostConfig { UserVariableStart = 300 };
|
||||
var output = PostNestWithVariables("width = 48.0\nG90\nG01X$widthY0", config);
|
||||
|
||||
Assert.Contains("#300=48", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CutOff_VerticalCut_UsesSheetWidthVariable()
|
||||
{
|
||||
// Create a plate with a vertical cutoff
|
||||
var config = new CincinnatiPostConfig { SheetWidthVariable = 110, SheetLengthVariable = 111 };
|
||||
var nest = new Nest { Name = "Test" };
|
||||
var plate = new Plate(new Size(48, 96));
|
||||
|
||||
// Add a simple part so the plate isn't empty
|
||||
var partPgm = new Program();
|
||||
partPgm.Codes.Add(new RapidMove(0, 0));
|
||||
partPgm.Codes.Add(new LinearMove(10, 0));
|
||||
partPgm.Codes.Add(new LinearMove(10, 10));
|
||||
partPgm.Codes.Add(new LinearMove(0, 10));
|
||||
partPgm.Codes.Add(new LinearMove(0, 0));
|
||||
var drawing = new Drawing("Part1", partPgm);
|
||||
nest.Drawings.Add(drawing);
|
||||
plate.Parts.Add(new Part(drawing, new Vector(0, 0)));
|
||||
|
||||
// Add a vertical cutoff that goes full width (Y=0 to Y=48)
|
||||
var cutoff = new CutOff(new Vector(20, 0), CutOffAxis.Vertical);
|
||||
plate.CutOffs.Add(cutoff);
|
||||
plate.RegenerateCutOffs(new CutOffSettings());
|
||||
|
||||
nest.Plates.Add(plate);
|
||||
|
||||
var post = new CincinnatiPostProcessor(config);
|
||||
var output = PostToString(post, nest);
|
||||
|
||||
// The cutoff line end at Y=48 (sheet width) should use #110
|
||||
Assert.Contains("Y#110", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CutOff_SegmentedCut_OnlyEdgeUsesVariable()
|
||||
{
|
||||
// Create a plate with a part in the middle and a vertical cutoff
|
||||
var config = new CincinnatiPostConfig { SheetWidthVariable = 110 };
|
||||
var nest = new Nest { Name = "Test" };
|
||||
var plate = new Plate(new Size(48, 96));
|
||||
|
||||
// Part in the middle — cutoff will be segmented around it
|
||||
var partPgm = new Program();
|
||||
partPgm.Codes.Add(new RapidMove(0, 0));
|
||||
partPgm.Codes.Add(new LinearMove(10, 0));
|
||||
partPgm.Codes.Add(new LinearMove(10, 10));
|
||||
partPgm.Codes.Add(new LinearMove(0, 10));
|
||||
partPgm.Codes.Add(new LinearMove(0, 0));
|
||||
var drawing = new Drawing("Part1", partPgm);
|
||||
nest.Drawings.Add(drawing);
|
||||
plate.Parts.Add(new Part(drawing, new Vector(15, 20))); // Part at Y=20-30, should create gap
|
||||
|
||||
var cutoff = new CutOff(new Vector(20, 0), CutOffAxis.Vertical);
|
||||
plate.CutOffs.Add(cutoff);
|
||||
plate.RegenerateCutOffs(new CutOffSettings());
|
||||
|
||||
nest.Plates.Add(plate);
|
||||
|
||||
var post = new CincinnatiPostProcessor(config);
|
||||
var output = PostToString(post, nest);
|
||||
|
||||
// The last segment endpoint at Y=48 should use #110
|
||||
Assert.Contains("Y#110", output);
|
||||
}
|
||||
|
||||
private static string PostNestWithVariables(string gcode, CincinnatiPostConfig config = null)
|
||||
{
|
||||
var program = ParseProgram(gcode);
|
||||
var drawing = new Drawing("TestPart", program);
|
||||
var nest = new Nest { Name = "Test" };
|
||||
nest.Drawings.Add(drawing);
|
||||
var plate = new Plate(new Size(100, 100));
|
||||
plate.Parts.Add(new Part(drawing, new Vector(0, 0)));
|
||||
nest.Plates.Add(plate);
|
||||
|
||||
config ??= new CincinnatiPostConfig { UserVariableStart = 200 };
|
||||
var post = new CincinnatiPostProcessor(config);
|
||||
return PostToString(post, nest);
|
||||
}
|
||||
|
||||
private static string PostToString(CincinnatiPostProcessor post, Nest nest)
|
||||
{
|
||||
var ms = new MemoryStream();
|
||||
post.Post(nest, ms);
|
||||
ms.Position = 0;
|
||||
return new StreamReader(ms).ReadToEnd();
|
||||
}
|
||||
|
||||
private static Program ParseProgram(string gcode)
|
||||
{
|
||||
var stream = new MemoryStream(Encoding.UTF8.GetBytes(gcode));
|
||||
var reader = new ProgramReader(stream);
|
||||
var program = reader.Read();
|
||||
reader.Close();
|
||||
return program;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.CNC.CuttingStrategy;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Tests.CuttingStrategy;
|
||||
|
||||
public class ApplySingleTests
|
||||
{
|
||||
private static Program MakeSquareProgram()
|
||||
{
|
||||
var pgm = new Program();
|
||||
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(0, 10)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(10, 10)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(10, 0)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(0, 0)));
|
||||
return pgm;
|
||||
}
|
||||
|
||||
private static Program MakeSquareWithHoleProgram()
|
||||
{
|
||||
var pgm = new Program();
|
||||
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(0, 20)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(20, 20)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(20, 0)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(0, 0)));
|
||||
pgm.Codes.Add(new RapidMove(new Vector(12, 10)));
|
||||
pgm.Codes.Add(new ArcMove(new Vector(12, 10), new Vector(10, 10), RotationType.CW));
|
||||
return pgm;
|
||||
}
|
||||
|
||||
private static List<ICode> GetLeadInCodes(Program program)
|
||||
{
|
||||
var result = new List<ICode>();
|
||||
foreach (var code in program.Codes)
|
||||
{
|
||||
if (code is LinearMove lm && lm.Layer == LayerType.Leadin)
|
||||
result.Add(lm);
|
||||
else if (code is ArcMove am && am.Layer == LayerType.Leadin)
|
||||
result.Add(am);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplySingle_ExternalContour_PlacesLeadInAtExactPoint()
|
||||
{
|
||||
var pgm = MakeSquareProgram();
|
||||
var strategy = new ContourCuttingStrategy
|
||||
{
|
||||
Parameters = new CuttingParameters
|
||||
{
|
||||
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }
|
||||
}
|
||||
};
|
||||
|
||||
var clickPoint = new Vector(5, 0);
|
||||
var entity = new Line(new Vector(10, 0), new Vector(0, 0));
|
||||
var result = strategy.ApplySingle(pgm, clickPoint, entity, ContourType.External);
|
||||
|
||||
var hasLeadin = result.Program.Codes.OfType<LinearMove>().Any(m => m.Layer == LayerType.Leadin);
|
||||
Assert.True(hasLeadin);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplySingle_ContourStartsAtClickPoint()
|
||||
{
|
||||
var pgm = MakeSquareProgram();
|
||||
var strategy = new ContourCuttingStrategy
|
||||
{
|
||||
Parameters = new CuttingParameters
|
||||
{
|
||||
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }
|
||||
}
|
||||
};
|
||||
|
||||
var clickPoint = new Vector(5, 0);
|
||||
var entity = new Line(new Vector(10, 0), new Vector(0, 0));
|
||||
var result = strategy.ApplySingle(pgm, clickPoint, entity, ContourType.External);
|
||||
|
||||
// Convert back to absolute to check positions
|
||||
result.Program.Mode = Mode.Absolute;
|
||||
|
||||
var firstLinear = result.Program.Codes.OfType<LinearMove>()
|
||||
.First(m => m.Layer == LayerType.Leadin);
|
||||
Assert.Equal(clickPoint.X, firstLinear.EndPoint.X, 4);
|
||||
Assert.Equal(clickPoint.Y, firstLinear.EndPoint.Y, 4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplySingle_ProgramModeIsIncremental()
|
||||
{
|
||||
var pgm = MakeSquareProgram();
|
||||
var strategy = new ContourCuttingStrategy
|
||||
{
|
||||
Parameters = new CuttingParameters
|
||||
{
|
||||
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }
|
||||
}
|
||||
};
|
||||
|
||||
var clickPoint = new Vector(5, 0);
|
||||
var entity = new Line(new Vector(10, 0), new Vector(0, 0));
|
||||
var result = strategy.ApplySingle(pgm, clickPoint, entity, ContourType.External);
|
||||
|
||||
Assert.Equal(Mode.Incremental, result.Program.Mode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplySingle_OnlyTargetContourGetsLeadIn()
|
||||
{
|
||||
var pgm = MakeSquareWithHoleProgram();
|
||||
var strategy = new ContourCuttingStrategy
|
||||
{
|
||||
Parameters = new CuttingParameters
|
||||
{
|
||||
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 },
|
||||
InternalLeadIn = new LineLeadIn { Length = 0.25, ApproachAngle = 90 }
|
||||
}
|
||||
};
|
||||
|
||||
var clickPoint = new Vector(10, 0);
|
||||
var entity = new Line(new Vector(20, 0), new Vector(0, 0));
|
||||
var result = strategy.ApplySingle(pgm, clickPoint, entity, ContourType.External);
|
||||
|
||||
var leadinMoves = GetLeadInCodes(result.Program);
|
||||
Assert.Single(leadinMoves);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using OpenNest.CNC.CuttingStrategy;
|
||||
using OpenNest.Forms;
|
||||
|
||||
namespace OpenNest.Tests.CuttingStrategy;
|
||||
|
||||
public class CuttingParametersSerializerTests
|
||||
{
|
||||
[Fact]
|
||||
public void RoundTrip_PreservesAutoTabFields()
|
||||
{
|
||||
var original = new CuttingParameters
|
||||
{
|
||||
AutoTabMinSize = 0.5,
|
||||
AutoTabMaxSize = 3.0,
|
||||
ExternalLeadIn = new LineLeadIn { Length = 0.25, ApproachAngle = 90 }
|
||||
};
|
||||
|
||||
var json = CuttingParametersSerializer.Serialize(original);
|
||||
var restored = CuttingParametersSerializer.Deserialize(json);
|
||||
|
||||
Assert.Equal(0.5, restored.AutoTabMinSize);
|
||||
Assert.Equal(3.0, restored.AutoTabMaxSize);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Deserialize_MissingAutoTabFields_DefaultsToZero()
|
||||
{
|
||||
var json = "{\"externalLeadIn\":{\"type\":\"None\"},\"externalLeadOut\":{\"type\":\"None\"},\"internalLeadIn\":{\"type\":\"None\"},\"internalLeadOut\":{\"type\":\"None\"},\"arcCircleLeadIn\":{\"type\":\"None\"},\"arcCircleLeadOut\":{\"type\":\"None\"},\"tabsEnabled\":false,\"tabWidth\":0.25,\"pierceClearance\":0.0625}";
|
||||
|
||||
var restored = CuttingParametersSerializer.Deserialize(json);
|
||||
|
||||
Assert.Equal(0.0, restored.AutoTabMinSize);
|
||||
Assert.Equal(0.0, restored.AutoTabMaxSize);
|
||||
}
|
||||
}
|
||||
@@ -124,4 +124,74 @@ public class PartLeadInTests
|
||||
var part = MakeSquarePart();
|
||||
Assert.False(part.LeadInsLocked);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplySingleLeadIn_SetsHasManualLeadIns()
|
||||
{
|
||||
var part = MakeSquarePart();
|
||||
var parameters = new CuttingParameters
|
||||
{
|
||||
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }
|
||||
};
|
||||
|
||||
var entity = new Line(new Vector(10, 0), new Vector(0, 0));
|
||||
part.ApplySingleLeadIn(parameters, new Vector(5, 0), entity, ContourType.External);
|
||||
|
||||
Assert.True(part.HasManualLeadIns);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplySingleLeadIn_ProgramContainsLeadinCodes()
|
||||
{
|
||||
var part = MakeSquarePart();
|
||||
var parameters = new CuttingParameters
|
||||
{
|
||||
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }
|
||||
};
|
||||
|
||||
var entity = new Line(new Vector(10, 0), new Vector(0, 0));
|
||||
part.ApplySingleLeadIn(parameters, new Vector(5, 0), entity, ContourType.External);
|
||||
|
||||
var hasLeadin = part.Program.Codes.OfType<LinearMove>().Any(m => m.Layer == LayerType.Leadin);
|
||||
Assert.True(hasLeadin);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplySingleLeadIn_ThenRemove_RestoresOriginal()
|
||||
{
|
||||
var part = MakeSquarePart();
|
||||
var originalCodeCount = part.Program.Codes.Count;
|
||||
var parameters = new CuttingParameters
|
||||
{
|
||||
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }
|
||||
};
|
||||
|
||||
var entity = new Line(new Vector(10, 0), new Vector(0, 0));
|
||||
part.ApplySingleLeadIn(parameters, new Vector(5, 0), entity, ContourType.External);
|
||||
part.RemoveLeadIns();
|
||||
|
||||
Assert.False(part.HasManualLeadIns);
|
||||
Assert.Equal(originalCodeCount, part.Program.Codes.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplySingleLeadIn_PreservesRotation()
|
||||
{
|
||||
var part = MakeSquarePart();
|
||||
part.Rotate(System.Math.PI / 4);
|
||||
var rotation = part.Rotation;
|
||||
|
||||
var parameters = new CuttingParameters
|
||||
{
|
||||
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }
|
||||
};
|
||||
|
||||
// After rotation, the edges change. Use a point on the rotated bottom edge.
|
||||
// The rotated square has vertices roughly at rotated positions.
|
||||
// We'll use a generic entity that will be matched via fallback.
|
||||
var entity = new Line(new Vector(10, 0), new Vector(0, 0));
|
||||
part.ApplySingleLeadIn(parameters, new Vector(5, 0), entity, ContourType.External);
|
||||
|
||||
Assert.Equal(rotation, part.Rotation, 6);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,8 +15,11 @@ public class EngineOverlapTests
|
||||
_output = output;
|
||||
}
|
||||
|
||||
private static Drawing ImportDxf()
|
||||
private static Drawing? ImportDxf()
|
||||
{
|
||||
if (!System.IO.File.Exists(DxfPath))
|
||||
return null;
|
||||
|
||||
var importer = new DxfImporter();
|
||||
importer.GetGeometry(DxfPath, out var geometry);
|
||||
var pgm = ConvertGeometry.ToProgram(geometry);
|
||||
@@ -31,6 +34,9 @@ public class EngineOverlapTests
|
||||
public void FillPlate_NoOverlaps(string engineName)
|
||||
{
|
||||
var drawing = ImportDxf();
|
||||
if (drawing is null)
|
||||
return; // Skip if test DXF not available
|
||||
|
||||
var plate = new Plate(60, 120);
|
||||
|
||||
NestEngineRegistry.ActiveEngineName = engineName;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
@@ -7,11 +8,11 @@ public class PatternTilerTests
|
||||
{
|
||||
private static Drawing MakeSquareDrawing(double size)
|
||||
{
|
||||
var pgm = new CNC.Program();
|
||||
pgm.Codes.Add(new CNC.LinearMove(size, 0));
|
||||
pgm.Codes.Add(new CNC.LinearMove(size, size));
|
||||
pgm.Codes.Add(new CNC.LinearMove(0, size));
|
||||
pgm.Codes.Add(new CNC.LinearMove(0, 0));
|
||||
var pgm = new Program();
|
||||
pgm.Codes.Add(new LinearMove(size, 0));
|
||||
pgm.Codes.Add(new LinearMove(size, size));
|
||||
pgm.Codes.Add(new LinearMove(0, size));
|
||||
pgm.Codes.Add(new LinearMove(0, 0));
|
||||
return new Drawing("square", pgm);
|
||||
}
|
||||
|
||||
|
||||
@@ -57,12 +57,12 @@ public class PolygonHelperTests
|
||||
public void ExtractPerimeterPolygon_InflatedPolygonIsLarger_ForCCWWinding()
|
||||
{
|
||||
// CCW winding: (0,0)→(10,0)→(10,10)→(0,10)→(0,0)
|
||||
var pgm = new CNC.Program();
|
||||
pgm.Codes.Add(new CNC.RapidMove(new Vector(0, 0)));
|
||||
pgm.Codes.Add(new CNC.LinearMove(new Vector(10, 0)));
|
||||
pgm.Codes.Add(new CNC.LinearMove(new Vector(10, 10)));
|
||||
pgm.Codes.Add(new CNC.LinearMove(new Vector(0, 10)));
|
||||
pgm.Codes.Add(new CNC.LinearMove(new Vector(0, 0)));
|
||||
var pgm = new Program();
|
||||
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(10, 0)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(10, 10)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(0, 10)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(0, 0)));
|
||||
var drawing = new Drawing("ccw-square", pgm);
|
||||
|
||||
var noSpacing = PolygonHelper.ExtractPerimeterPolygon(drawing, 0);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.Tests.Math;
|
||||
|
||||
public class ExpressionEvaluatorTests
|
||||
{
|
||||
private static readonly Dictionary<string, double> Empty = new();
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_NumericLiteral()
|
||||
{
|
||||
Assert.Equal(42.0, ExpressionEvaluator.Evaluate("42", Empty));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_DecimalLiteral()
|
||||
{
|
||||
Assert.Equal(0.3, ExpressionEvaluator.Evaluate("0.3", Empty));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_NegativeLiteral()
|
||||
{
|
||||
Assert.Equal(-5.0, ExpressionEvaluator.Evaluate("-5", Empty));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_Addition()
|
||||
{
|
||||
Assert.Equal(5.0, ExpressionEvaluator.Evaluate("2 + 3", Empty));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_Subtraction()
|
||||
{
|
||||
Assert.Equal(1.0, ExpressionEvaluator.Evaluate("3 - 2", Empty));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_Multiplication()
|
||||
{
|
||||
Assert.Equal(6.0, ExpressionEvaluator.Evaluate("2 * 3", Empty));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_Division()
|
||||
{
|
||||
Assert.Equal(2.5, ExpressionEvaluator.Evaluate("5 / 2", Empty));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_OperatorPrecedence()
|
||||
{
|
||||
Assert.Equal(7.0, ExpressionEvaluator.Evaluate("1 + 2 * 3", Empty));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_Parentheses()
|
||||
{
|
||||
Assert.Equal(9.0, ExpressionEvaluator.Evaluate("(1 + 2) * 3", Empty));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_VariableReference()
|
||||
{
|
||||
var vars = new Dictionary<string, double> { { "diameter", 0.3 } };
|
||||
Assert.Equal(0.3, ExpressionEvaluator.Evaluate("$diameter", vars));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_VariableInExpression()
|
||||
{
|
||||
var vars = new Dictionary<string, double> { { "diameter", 0.6 } };
|
||||
Assert.Equal(0.3, ExpressionEvaluator.Evaluate("$diameter / 2", vars));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_MultipleVariables()
|
||||
{
|
||||
var vars = new Dictionary<string, double> { { "x", 2.0 }, { "y", 3.0 } };
|
||||
Assert.Equal(5.0, ExpressionEvaluator.Evaluate("$x + $y", vars));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_CaseInsensitiveVariables()
|
||||
{
|
||||
var vars = new Dictionary<string, double>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "Diameter", 0.3 }
|
||||
};
|
||||
Assert.Equal(0.3, ExpressionEvaluator.Evaluate("$diameter", vars));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_UndefinedVariable_Throws()
|
||||
{
|
||||
Assert.Throws<KeyNotFoundException>(() =>
|
||||
ExpressionEvaluator.Evaluate("$missing", Empty));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_NestedParentheses()
|
||||
{
|
||||
Assert.Equal(20.0, ExpressionEvaluator.Evaluate("((2 + 3) * (1 + 1)) * 2", Empty));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_UnaryMinusBeforeParens()
|
||||
{
|
||||
Assert.Equal(-6.0, ExpressionEvaluator.Evaluate("-(2 + 4)", Empty));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_WhitespaceHandling()
|
||||
{
|
||||
Assert.Equal(5.0, ExpressionEvaluator.Evaluate(" 2 + 3 ", Empty));
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@
|
||||
<ProjectReference Include="..\OpenNest.Engine\OpenNest.Engine.csproj" />
|
||||
<ProjectReference Include="..\OpenNest.IO\OpenNest.IO.csproj" />
|
||||
<ProjectReference Include="..\OpenNest.Posts.Cincinnati\OpenNest.Posts.Cincinnati.csproj" />
|
||||
<ProjectReference Include="..\OpenNest\OpenNest.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -18,8 +18,11 @@ public class StrategyOverlapTests
|
||||
_output = output;
|
||||
}
|
||||
|
||||
private static Drawing ImportDxf()
|
||||
private static Drawing? ImportDxf()
|
||||
{
|
||||
if (!System.IO.File.Exists(DxfPath))
|
||||
return null;
|
||||
|
||||
var importer = new DxfImporter();
|
||||
importer.GetGeometry(DxfPath, out var geometry);
|
||||
var pgm = ConvertGeometry.ToProgram(geometry);
|
||||
@@ -30,6 +33,9 @@ public class StrategyOverlapTests
|
||||
public void EachStrategy_CheckOverlaps()
|
||||
{
|
||||
var drawing = ImportDxf();
|
||||
if (drawing is null)
|
||||
return; // Skip if test DXF not available
|
||||
|
||||
_output.WriteLine($"Drawing bbox: {drawing.Program.BoundingBox().Width:F2} x {drawing.Program.BoundingBox().Length:F2}");
|
||||
|
||||
var strategies = FillStrategyRegistry.Strategies.ToList();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using OpenNest.CNC.CuttingStrategy;
|
||||
using OpenNest.Controls;
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Forms;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Generic;
|
||||
@@ -30,9 +31,12 @@ namespace OpenNest.Actions
|
||||
private bool hasSnap;
|
||||
private SnapType activeSnapType;
|
||||
private ShapeInfo hoveredContour;
|
||||
private ShapeInfo lockedContour;
|
||||
private ContextMenuStrip contextMenu;
|
||||
private CuttingPanel cuttingPanel;
|
||||
private static readonly Brush grayOverlay = new SolidBrush(Color.FromArgb(160, 180, 180, 180));
|
||||
private static readonly Pen highlightPen = new Pen(Color.Cyan, 2.5f);
|
||||
private static readonly Pen lockedPen = new Pen(Color.Yellow, 3.0f);
|
||||
|
||||
public ActionLeadIn(PlateView plateView)
|
||||
: base(plateView)
|
||||
@@ -46,6 +50,7 @@ namespace OpenNest.Actions
|
||||
plateView.MouseDown += OnMouseDown;
|
||||
plateView.KeyDown += OnKeyDown;
|
||||
plateView.Paint += OnPaint;
|
||||
ShowSidePanel();
|
||||
}
|
||||
|
||||
public override void DisconnectEvents()
|
||||
@@ -55,6 +60,8 @@ namespace OpenNest.Actions
|
||||
plateView.KeyDown -= OnKeyDown;
|
||||
plateView.Paint -= OnPaint;
|
||||
|
||||
HideSidePanel();
|
||||
|
||||
contextMenu?.Dispose();
|
||||
contextMenu = null;
|
||||
|
||||
@@ -72,6 +79,77 @@ namespace OpenNest.Actions
|
||||
|
||||
public override bool IsBusy() => selectedPart != null;
|
||||
|
||||
private void ShowSidePanel()
|
||||
{
|
||||
var form = plateView.FindForm() as EditNestForm;
|
||||
if (form == null)
|
||||
return;
|
||||
|
||||
cuttingPanel = new CuttingPanel { ShowAutoAssign = true };
|
||||
cuttingPanel.AutoAssignClicked += OnAutoAssignClicked;
|
||||
cuttingPanel.ParametersChanged += OnToolParametersChanged;
|
||||
|
||||
// Load current parameters or defaults
|
||||
var plate = plateView.Plate;
|
||||
if (plate?.CuttingParameters != null)
|
||||
cuttingPanel.LoadFromParameters(plate.CuttingParameters);
|
||||
else
|
||||
{
|
||||
var json = Properties.Settings.Default.CuttingParametersJson;
|
||||
if (!string.IsNullOrEmpty(json))
|
||||
{
|
||||
try
|
||||
{
|
||||
var saved = CuttingParametersSerializer.Deserialize(json);
|
||||
cuttingPanel.LoadFromParameters(saved);
|
||||
}
|
||||
catch { /* use defaults */ }
|
||||
}
|
||||
}
|
||||
|
||||
form.ShowSidePanel(cuttingPanel);
|
||||
}
|
||||
|
||||
private void HideSidePanel()
|
||||
{
|
||||
if (cuttingPanel == null)
|
||||
return;
|
||||
|
||||
SaveParameters();
|
||||
|
||||
cuttingPanel.ParametersChanged -= OnToolParametersChanged;
|
||||
cuttingPanel.AutoAssignClicked -= OnAutoAssignClicked;
|
||||
|
||||
var form = plateView.FindForm() as EditNestForm;
|
||||
form?.HideSidePanel();
|
||||
|
||||
cuttingPanel = null;
|
||||
}
|
||||
|
||||
private CuttingParameters GetCurrentParameters()
|
||||
{
|
||||
return cuttingPanel?.BuildParameters() ?? plateView.Plate?.CuttingParameters ?? new CuttingParameters();
|
||||
}
|
||||
|
||||
private void SaveParameters()
|
||||
{
|
||||
if (cuttingPanel == null)
|
||||
return;
|
||||
|
||||
var parameters = cuttingPanel.BuildParameters();
|
||||
var json = CuttingParametersSerializer.Serialize(parameters);
|
||||
Properties.Settings.Default.CuttingParametersJson = json;
|
||||
Properties.Settings.Default.Save();
|
||||
|
||||
if (plateView.Plate != null)
|
||||
plateView.Plate.CuttingParameters = parameters;
|
||||
}
|
||||
|
||||
private void OnToolParametersChanged(object sender, System.EventArgs e)
|
||||
{
|
||||
plateView.Invalidate();
|
||||
}
|
||||
|
||||
private void OnMouseMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (selectedPart == null || contours == null)
|
||||
@@ -91,7 +169,12 @@ namespace OpenNest.Actions
|
||||
activeSnapType = SnapType.None;
|
||||
hoveredContour = null;
|
||||
|
||||
foreach (var info in contours)
|
||||
// When a contour is locked, only snap within that contour
|
||||
var searchContours = lockedContour != null
|
||||
? new List<ShapeInfo> { lockedContour }
|
||||
: contours;
|
||||
|
||||
foreach (var info in searchContours)
|
||||
{
|
||||
var closest = info.Shape.ClosestPointTo(localPt, out var entity);
|
||||
var dist = closest.DistanceTo(localPt);
|
||||
@@ -102,7 +185,7 @@ namespace OpenNest.Actions
|
||||
snapPoint = closest;
|
||||
snapEntity = entity;
|
||||
snapContourType = info.ContourType;
|
||||
snapNormal = ContourCuttingStrategy.ComputeNormal(closest, entity, info.ContourType);
|
||||
snapNormal = ContourCuttingStrategy.ComputeNormal(closest, entity, info.ContourType, info.Winding);
|
||||
hasSnap = true;
|
||||
hoveredContour = info;
|
||||
}
|
||||
@@ -110,8 +193,14 @@ namespace OpenNest.Actions
|
||||
|
||||
// Check endpoint/midpoint snaps on the hovered contour
|
||||
if (hoveredContour != null)
|
||||
{
|
||||
TrySnapToEntityPoints(localPt);
|
||||
|
||||
// Auto-switch tool window tab only when no contour is locked
|
||||
if (cuttingPanel != null && lockedContour == null)
|
||||
cuttingPanel.ActiveContourType = snapContourType;
|
||||
}
|
||||
|
||||
plateView.Invalidate();
|
||||
}
|
||||
|
||||
@@ -124,15 +213,22 @@ namespace OpenNest.Actions
|
||||
// First click: select a part
|
||||
SelectPartAtCursor();
|
||||
}
|
||||
else if (hasSnap)
|
||||
else if (lockedContour == null && hasSnap)
|
||||
{
|
||||
// Second click: commit lead-in at snap point
|
||||
// Second click: lock the hovered contour
|
||||
LockContour(hoveredContour);
|
||||
}
|
||||
else if (lockedContour != null && hasSnap)
|
||||
{
|
||||
// Third click: commit lead-in at snap point on locked contour
|
||||
CommitLeadIn();
|
||||
}
|
||||
}
|
||||
else if (e.Button == MouseButtons.Right)
|
||||
{
|
||||
if (selectedPart != null && selectedPart.HasManualLeadIns)
|
||||
if (lockedContour != null)
|
||||
UnlockContour();
|
||||
else if (selectedPart != null && selectedPart.HasManualLeadIns)
|
||||
ShowContextMenu(e.Location);
|
||||
else
|
||||
DeselectPart();
|
||||
@@ -143,7 +239,9 @@ namespace OpenNest.Actions
|
||||
{
|
||||
if (e.KeyCode == Keys.Escape)
|
||||
{
|
||||
if (selectedPart != null)
|
||||
if (lockedContour != null)
|
||||
UnlockContour();
|
||||
else if (selectedPart != null)
|
||||
DeselectPart();
|
||||
else
|
||||
plateView.SetAction(typeof(ActionSelect));
|
||||
@@ -159,6 +257,23 @@ namespace OpenNest.Actions
|
||||
DrawLeadInPreview(g);
|
||||
}
|
||||
|
||||
private void LockContour(ShapeInfo contour)
|
||||
{
|
||||
lockedContour = contour;
|
||||
|
||||
// Lock the tab to this contour type
|
||||
if (cuttingPanel != null)
|
||||
cuttingPanel.ActiveContourType = contour.ContourType;
|
||||
|
||||
plateView.Invalidate();
|
||||
}
|
||||
|
||||
private void UnlockContour()
|
||||
{
|
||||
lockedContour = null;
|
||||
plateView.Invalidate();
|
||||
}
|
||||
|
||||
private void DrawOverlay(Graphics g)
|
||||
{
|
||||
foreach (var lp in plateView.LayoutParts)
|
||||
@@ -170,10 +285,24 @@ namespace OpenNest.Actions
|
||||
|
||||
private void DrawHoveredContour(Graphics g)
|
||||
{
|
||||
if (hoveredContour == null || selectedPart == null)
|
||||
if (selectedPart == null)
|
||||
return;
|
||||
|
||||
using var contourPath = hoveredContour.Shape.GetGraphicsPath();
|
||||
// Draw locked contour with distinct pen
|
||||
if (lockedContour != null)
|
||||
{
|
||||
DrawContourHighlight(g, lockedContour.Shape, lockedPen);
|
||||
return;
|
||||
}
|
||||
|
||||
// Draw hovered contour
|
||||
if (hoveredContour != null)
|
||||
DrawContourHighlight(g, hoveredContour.Shape, highlightPen);
|
||||
}
|
||||
|
||||
private void DrawContourHighlight(Graphics g, Shape shape, Pen pen)
|
||||
{
|
||||
using var contourPath = shape.GetGraphicsPath();
|
||||
using var contourMatrix = new Matrix();
|
||||
contourMatrix.Translate((float)selectedPart.Location.X, (float)selectedPart.Location.Y);
|
||||
contourMatrix.Multiply(plateView.Matrix, MatrixOrder.Append);
|
||||
@@ -181,7 +310,7 @@ namespace OpenNest.Actions
|
||||
|
||||
var prevSmooth = g.SmoothingMode;
|
||||
g.SmoothingMode = SmoothingMode.AntiAlias;
|
||||
g.DrawPath(highlightPen, contourPath);
|
||||
g.DrawPath(pen, contourPath);
|
||||
g.SmoothingMode = prevSmooth;
|
||||
}
|
||||
|
||||
@@ -190,9 +319,7 @@ namespace OpenNest.Actions
|
||||
if (!hasSnap || selectedPart == null)
|
||||
return;
|
||||
|
||||
var parameters = plateView.Plate?.CuttingParameters;
|
||||
if (parameters == null)
|
||||
return;
|
||||
var parameters = GetCurrentParameters();
|
||||
|
||||
var leadIn = SelectLeadIn(parameters, snapContourType);
|
||||
if (leadIn == null)
|
||||
@@ -282,7 +409,7 @@ namespace OpenNest.Actions
|
||||
{
|
||||
snapPoint = bestPoint;
|
||||
snapEntity = bestEntity;
|
||||
snapNormal = ContourCuttingStrategy.ComputeNormal(bestPoint, bestEntity, snapContourType);
|
||||
snapNormal = ContourCuttingStrategy.ComputeNormal(bestPoint, bestEntity, snapContourType, hoveredContour.Winding);
|
||||
activeSnapType = bestType;
|
||||
}
|
||||
|
||||
@@ -356,7 +483,8 @@ namespace OpenNest.Actions
|
||||
contours.Add(new ShapeInfo
|
||||
{
|
||||
Shape = profile.Perimeter,
|
||||
ContourType = ContourType.External
|
||||
ContourType = ContourType.External,
|
||||
Winding = ContourCuttingStrategy.DetermineWinding(profile.Perimeter)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -366,36 +494,61 @@ namespace OpenNest.Actions
|
||||
contours.Add(new ShapeInfo
|
||||
{
|
||||
Shape = cutout,
|
||||
ContourType = ContourCuttingStrategy.DetectContourType(cutout)
|
||||
ContourType = ContourCuttingStrategy.DetectContourType(cutout),
|
||||
Winding = ContourCuttingStrategy.DetermineWinding(cutout)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void CommitLeadIn()
|
||||
{
|
||||
var parameters = plateView.Plate?.CuttingParameters;
|
||||
if (parameters == null)
|
||||
return;
|
||||
var parameters = GetCurrentParameters();
|
||||
|
||||
// Remove any existing lead-ins first
|
||||
if (selectedPart.HasManualLeadIns)
|
||||
selectedPart.RemoveLeadIns();
|
||||
|
||||
// Apply lead-ins using the snap point as the approach point.
|
||||
// snapPoint is in the program's local coordinate space (rotated, not offset),
|
||||
// which is what Part.ApplyLeadIns expects.
|
||||
selectedPart.ApplyLeadIns(parameters, snapPoint);
|
||||
ApplyAutoTab(parameters);
|
||||
|
||||
selectedPart.ApplySingleLeadIn(parameters, snapPoint, snapEntity, snapContourType);
|
||||
selectedPart.LeadInsLocked = true;
|
||||
|
||||
// Rebuild the layout part's graphics
|
||||
selectedLayoutPart.IsDirty = true;
|
||||
selectedLayoutPart.Update();
|
||||
|
||||
// Deselect and reset
|
||||
DeselectPart();
|
||||
UnlockContour();
|
||||
plateView.Invalidate();
|
||||
}
|
||||
|
||||
private void OnAutoAssignClicked(object sender, System.EventArgs e)
|
||||
{
|
||||
if (selectedPart == null)
|
||||
return;
|
||||
|
||||
var parameters = GetCurrentParameters();
|
||||
|
||||
if (selectedPart.HasManualLeadIns)
|
||||
selectedPart.RemoveLeadIns();
|
||||
|
||||
ApplyAutoTab(parameters);
|
||||
|
||||
selectedPart.ApplyLeadIns(parameters, Vector.Zero);
|
||||
selectedPart.LeadInsLocked = true;
|
||||
|
||||
selectedLayoutPart.IsDirty = true;
|
||||
UnlockContour();
|
||||
plateView.Invalidate();
|
||||
}
|
||||
|
||||
private void ApplyAutoTab(CuttingParameters parameters)
|
||||
{
|
||||
if (parameters.AutoTabMinSize <= 0 && parameters.AutoTabMaxSize <= 0)
|
||||
return;
|
||||
|
||||
var bbox = selectedPart.Program.BoundingBox();
|
||||
var minDim = System.Math.Min(bbox.Width, bbox.Length);
|
||||
|
||||
if (minDim >= parameters.AutoTabMinSize && minDim <= parameters.AutoTabMaxSize)
|
||||
parameters.TabsEnabled = true;
|
||||
}
|
||||
|
||||
private void DeselectPart()
|
||||
{
|
||||
if (selectedLayoutPart != null)
|
||||
@@ -407,6 +560,7 @@ namespace OpenNest.Actions
|
||||
selectedPart = null;
|
||||
profile = null;
|
||||
contours = null;
|
||||
lockedContour = null;
|
||||
hasSnap = false;
|
||||
activeSnapType = SnapType.None;
|
||||
hoveredContour = null;
|
||||
@@ -483,6 +637,7 @@ namespace OpenNest.Actions
|
||||
{
|
||||
public Shape Shape { get; set; }
|
||||
public ContourType ContourType { get; set; }
|
||||
public RotationType Winding { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
using OpenNest.CNC.CuttingStrategy;
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace OpenNest.Forms
|
||||
namespace OpenNest.Controls
|
||||
{
|
||||
public partial class CuttingParametersForm : Form
|
||||
public class CuttingPanel : Panel
|
||||
{
|
||||
private static readonly string[] LeadInTypes =
|
||||
{ "None", "Line", "Arc", "Line + Arc", "Clean Hole", "Line + Line" };
|
||||
@@ -12,85 +13,241 @@ namespace OpenNest.Forms
|
||||
private static readonly string[] LeadOutTypes =
|
||||
{ "None", "Line", "Arc", "Microtab" };
|
||||
|
||||
private ComboBox cboExternalLeadIn, cboExternalLeadOut;
|
||||
private ComboBox cboInternalLeadIn, cboInternalLeadOut;
|
||||
private ComboBox cboArcCircleLeadIn, cboArcCircleLeadOut;
|
||||
private readonly TabControl tabControl;
|
||||
private readonly ComboBox cboExternalLeadIn, cboExternalLeadOut;
|
||||
private readonly ComboBox cboInternalLeadIn, cboInternalLeadOut;
|
||||
private readonly ComboBox cboArcCircleLeadIn, cboArcCircleLeadOut;
|
||||
|
||||
private Panel pnlExternalLeadIn, pnlExternalLeadOut;
|
||||
private Panel pnlInternalLeadIn, pnlInternalLeadOut;
|
||||
private Panel pnlArcCircleLeadIn, pnlArcCircleLeadOut;
|
||||
private readonly Panel pnlExternalLeadIn, pnlExternalLeadOut;
|
||||
private readonly Panel pnlInternalLeadIn, pnlInternalLeadOut;
|
||||
private readonly Panel pnlArcCircleLeadIn, pnlArcCircleLeadOut;
|
||||
|
||||
private CheckBox chkTabsEnabled;
|
||||
private NumericUpDown nudTabWidth;
|
||||
private NumericUpDown nudPierceClearance;
|
||||
private readonly CheckBox chkTabsEnabled;
|
||||
private readonly NumericUpDown nudTabWidth;
|
||||
private readonly NumericUpDown nudAutoTabMin;
|
||||
private readonly NumericUpDown nudAutoTabMax;
|
||||
private readonly NumericUpDown nudPierceClearance;
|
||||
|
||||
private bool hasCustomParameters;
|
||||
private CuttingParameters parameters = new CuttingParameters();
|
||||
private readonly Button btnAutoAssign;
|
||||
|
||||
public CuttingParameters Parameters
|
||||
private bool suppressEvents;
|
||||
|
||||
public event EventHandler ParametersChanged;
|
||||
public event EventHandler AutoAssignClicked;
|
||||
|
||||
public bool ShowAutoAssign
|
||||
{
|
||||
get => parameters;
|
||||
get => btnAutoAssign.Visible;
|
||||
set => btnAutoAssign.Visible = value;
|
||||
}
|
||||
|
||||
public ContourType? ActiveContourType
|
||||
{
|
||||
get
|
||||
{
|
||||
return tabControl.SelectedIndex switch
|
||||
{
|
||||
0 => ContourType.External,
|
||||
1 => ContourType.Internal,
|
||||
2 => ContourType.ArcCircle,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
set
|
||||
{
|
||||
parameters = value;
|
||||
hasCustomParameters = true;
|
||||
if (value == null)
|
||||
return;
|
||||
|
||||
var index = value.Value switch
|
||||
{
|
||||
ContourType.External => 0,
|
||||
ContourType.Internal => 1,
|
||||
ContourType.ArcCircle => 2,
|
||||
_ => -1
|
||||
};
|
||||
|
||||
if (index >= 0 && tabControl.SelectedIndex != index)
|
||||
tabControl.SelectedIndex = index;
|
||||
}
|
||||
}
|
||||
|
||||
public CuttingParametersForm()
|
||||
public CuttingPanel()
|
||||
{
|
||||
InitializeComponent();
|
||||
AutoScroll = true;
|
||||
BackColor = Color.White;
|
||||
|
||||
SetupTab(tabExternal,
|
||||
out cboExternalLeadIn, out pnlExternalLeadIn,
|
||||
// Tab control for contour types — wrapped in a fixed-height panel for Dock.Top
|
||||
tabControl = new TabControl
|
||||
{
|
||||
Dock = DockStyle.Fill
|
||||
};
|
||||
|
||||
var tabExternal = new TabPage("External") { Padding = new Padding(4) };
|
||||
var tabInternal = new TabPage("Internal") { Padding = new Padding(4) };
|
||||
var tabArcCircle = new TabPage("Arc / Circle") { Padding = new Padding(4) };
|
||||
|
||||
SetupTab(tabExternal, out cboExternalLeadIn, out pnlExternalLeadIn,
|
||||
out cboExternalLeadOut, out pnlExternalLeadOut);
|
||||
SetupTab(tabInternal,
|
||||
out cboInternalLeadIn, out pnlInternalLeadIn,
|
||||
SetupTab(tabInternal, out cboInternalLeadIn, out pnlInternalLeadIn,
|
||||
out cboInternalLeadOut, out pnlInternalLeadOut);
|
||||
SetupTab(tabArcCircle,
|
||||
out cboArcCircleLeadIn, out pnlArcCircleLeadIn,
|
||||
SetupTab(tabArcCircle, out cboArcCircleLeadIn, out pnlArcCircleLeadIn,
|
||||
out cboArcCircleLeadOut, out pnlArcCircleLeadOut);
|
||||
|
||||
SetupTabsSection();
|
||||
tabControl.Controls.Add(tabExternal);
|
||||
tabControl.Controls.Add(tabInternal);
|
||||
tabControl.Controls.Add(tabArcCircle);
|
||||
|
||||
var tabWrapper = new Panel
|
||||
{
|
||||
Dock = DockStyle.Top,
|
||||
Height = 340
|
||||
};
|
||||
tabWrapper.Controls.Add(tabControl);
|
||||
|
||||
// Tabs section
|
||||
var tabsPanel = new CollapsiblePanel
|
||||
{
|
||||
HeaderText = "Tabs",
|
||||
Dock = DockStyle.Top,
|
||||
ExpandedHeight = 120,
|
||||
IsExpanded = false
|
||||
};
|
||||
|
||||
chkTabsEnabled = new CheckBox
|
||||
{
|
||||
Text = "Enable Tabs",
|
||||
Location = new Point(12, 4),
|
||||
AutoSize = true
|
||||
};
|
||||
chkTabsEnabled.CheckedChanged += (s, e) =>
|
||||
{
|
||||
nudTabWidth.Enabled = chkTabsEnabled.Checked;
|
||||
OnParametersChanged();
|
||||
};
|
||||
tabsPanel.ContentPanel.Controls.Add(chkTabsEnabled);
|
||||
|
||||
tabsPanel.ContentPanel.Controls.Add(new Label
|
||||
{
|
||||
Text = "Width:",
|
||||
Location = new Point(160, 6),
|
||||
AutoSize = true
|
||||
});
|
||||
|
||||
nudTabWidth = CreateNumeric(215, 3, 0.25, 0.0625);
|
||||
nudTabWidth.Enabled = false;
|
||||
tabsPanel.ContentPanel.Controls.Add(nudTabWidth);
|
||||
|
||||
tabsPanel.ContentPanel.Controls.Add(new Label
|
||||
{
|
||||
Text = "Auto-Tab Min Size:",
|
||||
Location = new Point(12, 32),
|
||||
AutoSize = true
|
||||
});
|
||||
|
||||
nudAutoTabMin = CreateNumeric(140, 29, 0, 0.0625);
|
||||
tabsPanel.ContentPanel.Controls.Add(nudAutoTabMin);
|
||||
|
||||
tabsPanel.ContentPanel.Controls.Add(new Label
|
||||
{
|
||||
Text = "Auto-Tab Max Size:",
|
||||
Location = new Point(12, 58),
|
||||
AutoSize = true
|
||||
});
|
||||
|
||||
nudAutoTabMax = CreateNumeric(140, 55, 0, 0.0625);
|
||||
tabsPanel.ContentPanel.Controls.Add(nudAutoTabMax);
|
||||
|
||||
// Pierce section
|
||||
var piercePanel = new CollapsiblePanel
|
||||
{
|
||||
HeaderText = "Pierce",
|
||||
Dock = DockStyle.Top,
|
||||
ExpandedHeight = 60,
|
||||
IsExpanded = true
|
||||
};
|
||||
|
||||
piercePanel.ContentPanel.Controls.Add(new Label
|
||||
{
|
||||
Text = "Pierce Clearance:",
|
||||
Location = new Point(12, 6),
|
||||
AutoSize = true
|
||||
});
|
||||
|
||||
nudPierceClearance = CreateNumeric(130, 3, 0.0625, 0.0625);
|
||||
piercePanel.ContentPanel.Controls.Add(nudPierceClearance);
|
||||
|
||||
// Auto-Assign button — wrapped in a panel for Dock.Top with padding
|
||||
btnAutoAssign = new Button
|
||||
{
|
||||
Text = "Auto-Assign Lead-ins",
|
||||
Dock = DockStyle.Top,
|
||||
Height = 32,
|
||||
Visible = false
|
||||
};
|
||||
btnAutoAssign.Click += (s, e) => AutoAssignClicked?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
var btnWrapper = new Panel
|
||||
{
|
||||
Dock = DockStyle.Top,
|
||||
Height = 36,
|
||||
Padding = new Padding(4, 2, 4, 2)
|
||||
};
|
||||
btnWrapper.Controls.Add(btnAutoAssign);
|
||||
|
||||
// Add in reverse order — Dock.Top stacks top-down
|
||||
Controls.Add(btnWrapper);
|
||||
Controls.Add(piercePanel);
|
||||
Controls.Add(tabsPanel);
|
||||
Controls.Add(tabWrapper);
|
||||
|
||||
// Wire up change events
|
||||
PopulateDropdowns();
|
||||
|
||||
cboExternalLeadIn.SelectedIndexChanged += OnLeadInTypeChanged;
|
||||
cboInternalLeadIn.SelectedIndexChanged += OnLeadInTypeChanged;
|
||||
cboArcCircleLeadIn.SelectedIndexChanged += OnLeadInTypeChanged;
|
||||
|
||||
cboExternalLeadOut.SelectedIndexChanged += OnLeadOutTypeChanged;
|
||||
cboInternalLeadOut.SelectedIndexChanged += OnLeadOutTypeChanged;
|
||||
cboArcCircleLeadOut.SelectedIndexChanged += OnLeadOutTypeChanged;
|
||||
WireChangeEvents();
|
||||
}
|
||||
|
||||
protected override void OnLoad(EventArgs e)
|
||||
public CuttingParameters BuildParameters()
|
||||
{
|
||||
base.OnLoad(e);
|
||||
|
||||
// If caller didn't provide custom parameters, try loading saved ones
|
||||
if (!hasCustomParameters)
|
||||
return new CuttingParameters
|
||||
{
|
||||
var json = Properties.Settings.Default.CuttingParametersJson;
|
||||
if (!string.IsNullOrEmpty(json))
|
||||
{
|
||||
try { Parameters = CuttingParametersSerializer.Deserialize(json); }
|
||||
catch { /* use defaults on corrupt data */ }
|
||||
}
|
||||
}
|
||||
|
||||
LoadFromParameters(Parameters);
|
||||
ExternalLeadIn = BuildLeadIn(cboExternalLeadIn, pnlExternalLeadIn),
|
||||
ExternalLeadOut = BuildLeadOut(cboExternalLeadOut, pnlExternalLeadOut),
|
||||
InternalLeadIn = BuildLeadIn(cboInternalLeadIn, pnlInternalLeadIn),
|
||||
InternalLeadOut = BuildLeadOut(cboInternalLeadOut, pnlInternalLeadOut),
|
||||
ArcCircleLeadIn = BuildLeadIn(cboArcCircleLeadIn, pnlArcCircleLeadIn),
|
||||
ArcCircleLeadOut = BuildLeadOut(cboArcCircleLeadOut, pnlArcCircleLeadOut),
|
||||
TabsEnabled = chkTabsEnabled.Checked,
|
||||
TabConfig = new NormalTab { Size = (double)nudTabWidth.Value },
|
||||
PierceClearance = (double)nudPierceClearance.Value,
|
||||
AutoTabMinSize = (double)nudAutoTabMin.Value,
|
||||
AutoTabMaxSize = (double)nudAutoTabMax.Value
|
||||
};
|
||||
}
|
||||
|
||||
protected override void OnFormClosing(FormClosingEventArgs e)
|
||||
public void LoadFromParameters(CuttingParameters p)
|
||||
{
|
||||
base.OnFormClosing(e);
|
||||
suppressEvents = true;
|
||||
|
||||
if (DialogResult == System.Windows.Forms.DialogResult.OK)
|
||||
{
|
||||
var json = CuttingParametersSerializer.Serialize(BuildParameters());
|
||||
Properties.Settings.Default.CuttingParametersJson = json;
|
||||
Properties.Settings.Default.Save();
|
||||
}
|
||||
LoadLeadIn(cboExternalLeadIn, pnlExternalLeadIn, p.ExternalLeadIn);
|
||||
LoadLeadOut(cboExternalLeadOut, pnlExternalLeadOut, p.ExternalLeadOut);
|
||||
LoadLeadIn(cboInternalLeadIn, pnlInternalLeadIn, p.InternalLeadIn);
|
||||
LoadLeadOut(cboInternalLeadOut, pnlInternalLeadOut, p.InternalLeadOut);
|
||||
LoadLeadIn(cboArcCircleLeadIn, pnlArcCircleLeadIn, p.ArcCircleLeadIn);
|
||||
LoadLeadOut(cboArcCircleLeadOut, pnlArcCircleLeadOut, p.ArcCircleLeadOut);
|
||||
|
||||
chkTabsEnabled.Checked = p.TabsEnabled;
|
||||
if (p.TabConfig != null)
|
||||
nudTabWidth.Value = (decimal)p.TabConfig.Size;
|
||||
nudPierceClearance.Value = (decimal)p.PierceClearance;
|
||||
nudAutoTabMin.Value = (decimal)p.AutoTabMinSize;
|
||||
nudAutoTabMax.Value = (decimal)p.AutoTabMaxSize;
|
||||
|
||||
suppressEvents = false;
|
||||
}
|
||||
|
||||
private void OnParametersChanged()
|
||||
{
|
||||
if (!suppressEvents)
|
||||
ParametersChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private static void SetupTab(TabPage tab,
|
||||
@@ -100,30 +257,30 @@ namespace OpenNest.Forms
|
||||
var grpLeadIn = new GroupBox
|
||||
{
|
||||
Text = "Lead-In",
|
||||
Location = new System.Drawing.Point(4, 4),
|
||||
Size = new System.Drawing.Size(364, 168)
|
||||
Location = new Point(4, 4),
|
||||
Size = new Size(340, 148)
|
||||
};
|
||||
tab.Controls.Add(grpLeadIn);
|
||||
|
||||
grpLeadIn.Controls.Add(new Label
|
||||
{
|
||||
Text = "Type:",
|
||||
Location = new System.Drawing.Point(8, 22),
|
||||
Location = new Point(8, 22),
|
||||
AutoSize = true
|
||||
});
|
||||
|
||||
leadInCombo = new ComboBox
|
||||
{
|
||||
DropDownStyle = ComboBoxStyle.DropDownList,
|
||||
Location = new System.Drawing.Point(90, 19),
|
||||
Size = new System.Drawing.Size(250, 24)
|
||||
Location = new Point(90, 19),
|
||||
Size = new Size(230, 24)
|
||||
};
|
||||
grpLeadIn.Controls.Add(leadInCombo);
|
||||
|
||||
leadInPanel = new Panel
|
||||
{
|
||||
Location = new System.Drawing.Point(8, 48),
|
||||
Size = new System.Drawing.Size(340, 112),
|
||||
Location = new Point(8, 48),
|
||||
Size = new Size(320, 92),
|
||||
AutoScroll = true
|
||||
};
|
||||
grpLeadIn.Controls.Add(leadInPanel);
|
||||
@@ -131,106 +288,35 @@ namespace OpenNest.Forms
|
||||
var grpLeadOut = new GroupBox
|
||||
{
|
||||
Text = "Lead-Out",
|
||||
Location = new System.Drawing.Point(4, 176),
|
||||
Size = new System.Drawing.Size(364, 132)
|
||||
Location = new Point(4, 156),
|
||||
Size = new Size(340, 132)
|
||||
};
|
||||
tab.Controls.Add(grpLeadOut);
|
||||
|
||||
grpLeadOut.Controls.Add(new Label
|
||||
{
|
||||
Text = "Type:",
|
||||
Location = new System.Drawing.Point(8, 22),
|
||||
Location = new Point(8, 22),
|
||||
AutoSize = true
|
||||
});
|
||||
|
||||
leadOutCombo = new ComboBox
|
||||
{
|
||||
DropDownStyle = ComboBoxStyle.DropDownList,
|
||||
Location = new System.Drawing.Point(90, 19),
|
||||
Size = new System.Drawing.Size(250, 24)
|
||||
Location = new Point(90, 19),
|
||||
Size = new Size(230, 24)
|
||||
};
|
||||
grpLeadOut.Controls.Add(leadOutCombo);
|
||||
|
||||
leadOutPanel = new Panel
|
||||
{
|
||||
Location = new System.Drawing.Point(8, 48),
|
||||
Size = new System.Drawing.Size(340, 76),
|
||||
Location = new Point(8, 48),
|
||||
Size = new Size(320, 76),
|
||||
AutoScroll = true
|
||||
};
|
||||
grpLeadOut.Controls.Add(leadOutPanel);
|
||||
}
|
||||
|
||||
private void SetupTabsSection()
|
||||
{
|
||||
var grpTabs = new GroupBox
|
||||
{
|
||||
Text = "Tabs",
|
||||
Location = new System.Drawing.Point(4, 350),
|
||||
Size = new System.Drawing.Size(372, 55),
|
||||
Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right
|
||||
};
|
||||
|
||||
chkTabsEnabled = new CheckBox
|
||||
{
|
||||
Text = "Enable Tabs",
|
||||
Location = new System.Drawing.Point(12, 22),
|
||||
AutoSize = true
|
||||
};
|
||||
chkTabsEnabled.CheckedChanged += (s, e) => nudTabWidth.Enabled = chkTabsEnabled.Checked;
|
||||
grpTabs.Controls.Add(chkTabsEnabled);
|
||||
|
||||
grpTabs.Controls.Add(new Label
|
||||
{
|
||||
Text = "Width:",
|
||||
Location = new System.Drawing.Point(160, 23),
|
||||
AutoSize = true
|
||||
});
|
||||
|
||||
nudTabWidth = new NumericUpDown
|
||||
{
|
||||
Location = new System.Drawing.Point(215, 20),
|
||||
Size = new System.Drawing.Size(100, 22),
|
||||
DecimalPlaces = 4,
|
||||
Increment = 0.0625m,
|
||||
Minimum = 0,
|
||||
Maximum = 9999,
|
||||
Value = 0.25m,
|
||||
Enabled = false
|
||||
};
|
||||
grpTabs.Controls.Add(nudTabWidth);
|
||||
|
||||
Controls.Add(grpTabs);
|
||||
|
||||
var grpPierce = new GroupBox
|
||||
{
|
||||
Text = "Pierce",
|
||||
Location = new System.Drawing.Point(4, 410),
|
||||
Size = new System.Drawing.Size(372, 55),
|
||||
Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right
|
||||
};
|
||||
|
||||
grpPierce.Controls.Add(new Label
|
||||
{
|
||||
Text = "Pierce Clearance:",
|
||||
Location = new System.Drawing.Point(12, 23),
|
||||
AutoSize = true
|
||||
});
|
||||
|
||||
nudPierceClearance = new NumericUpDown
|
||||
{
|
||||
Location = new System.Drawing.Point(130, 20),
|
||||
Size = new System.Drawing.Size(100, 22),
|
||||
DecimalPlaces = 4,
|
||||
Increment = 0.0625m,
|
||||
Minimum = 0,
|
||||
Maximum = 9999,
|
||||
Value = 0.0625m
|
||||
};
|
||||
grpPierce.Controls.Add(nudPierceClearance);
|
||||
|
||||
Controls.Add(grpPierce);
|
||||
}
|
||||
|
||||
private void PopulateDropdowns()
|
||||
{
|
||||
foreach (var combo in new[] { cboExternalLeadIn, cboInternalLeadIn, cboArcCircleLeadIn })
|
||||
@@ -246,12 +332,24 @@ namespace OpenNest.Forms
|
||||
}
|
||||
}
|
||||
|
||||
private void WireChangeEvents()
|
||||
{
|
||||
cboExternalLeadIn.SelectedIndexChanged += OnLeadInTypeChanged;
|
||||
cboInternalLeadIn.SelectedIndexChanged += OnLeadInTypeChanged;
|
||||
cboArcCircleLeadIn.SelectedIndexChanged += OnLeadInTypeChanged;
|
||||
|
||||
cboExternalLeadOut.SelectedIndexChanged += OnLeadOutTypeChanged;
|
||||
cboInternalLeadOut.SelectedIndexChanged += OnLeadOutTypeChanged;
|
||||
cboArcCircleLeadOut.SelectedIndexChanged += OnLeadOutTypeChanged;
|
||||
}
|
||||
|
||||
private void OnLeadInTypeChanged(object sender, EventArgs e)
|
||||
{
|
||||
var combo = (ComboBox)sender;
|
||||
var panel = GetLeadInPanel(combo);
|
||||
if (panel != null)
|
||||
BuildLeadInParamControls(panel, combo.SelectedIndex);
|
||||
OnParametersChanged();
|
||||
}
|
||||
|
||||
private void OnLeadOutTypeChanged(object sender, EventArgs e)
|
||||
@@ -260,6 +358,7 @@ namespace OpenNest.Forms
|
||||
var panel = GetLeadOutPanel(combo);
|
||||
if (panel != null)
|
||||
BuildLeadOutParamControls(panel, combo.SelectedIndex);
|
||||
OnParametersChanged();
|
||||
}
|
||||
|
||||
private Panel GetLeadInPanel(ComboBox combo)
|
||||
@@ -278,31 +377,31 @@ namespace OpenNest.Forms
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void BuildLeadInParamControls(Panel panel, int typeIndex)
|
||||
private void BuildLeadInParamControls(Panel panel, int typeIndex)
|
||||
{
|
||||
panel.Controls.Clear();
|
||||
var y = 0;
|
||||
|
||||
switch (typeIndex)
|
||||
{
|
||||
case 1: // Line
|
||||
case 1:
|
||||
AddNumericField(panel, "Length:", 0.25, ref y, "Length");
|
||||
AddNumericField(panel, "Approach Angle:", 90, ref y, "ApproachAngle");
|
||||
break;
|
||||
case 2: // Arc
|
||||
case 2:
|
||||
AddNumericField(panel, "Radius:", 0.25, ref y, "Radius");
|
||||
break;
|
||||
case 3: // Line + Arc
|
||||
case 3:
|
||||
AddNumericField(panel, "Line Length:", 0.25, ref y, "LineLength");
|
||||
AddNumericField(panel, "Arc Radius:", 0.125, ref y, "ArcRadius");
|
||||
AddNumericField(panel, "Approach Angle:", 135, ref y, "ApproachAngle");
|
||||
break;
|
||||
case 4: // Clean Hole
|
||||
case 4:
|
||||
AddNumericField(panel, "Line Length:", 0.25, ref y, "LineLength");
|
||||
AddNumericField(panel, "Arc Radius:", 0.125, ref y, "ArcRadius");
|
||||
AddNumericField(panel, "Kerf:", 0.06, ref y, "Kerf");
|
||||
break;
|
||||
case 5: // Line + Line
|
||||
case 5:
|
||||
AddNumericField(panel, "Length 1:", 0.25, ref y, "Length1");
|
||||
AddNumericField(panel, "Angle 1:", 90, ref y, "Angle1");
|
||||
AddNumericField(panel, "Length 2:", 0.25, ref y, "Length2");
|
||||
@@ -311,67 +410,56 @@ namespace OpenNest.Forms
|
||||
}
|
||||
}
|
||||
|
||||
private static void BuildLeadOutParamControls(Panel panel, int typeIndex)
|
||||
private void BuildLeadOutParamControls(Panel panel, int typeIndex)
|
||||
{
|
||||
panel.Controls.Clear();
|
||||
var y = 0;
|
||||
|
||||
switch (typeIndex)
|
||||
{
|
||||
case 1: // Line
|
||||
case 1:
|
||||
AddNumericField(panel, "Length:", 0.25, ref y, "Length");
|
||||
AddNumericField(panel, "Approach Angle:", 90, ref y, "ApproachAngle");
|
||||
break;
|
||||
case 2: // Arc
|
||||
case 2:
|
||||
AddNumericField(panel, "Radius:", 0.25, ref y, "Radius");
|
||||
break;
|
||||
case 3: // Microtab
|
||||
case 3:
|
||||
AddNumericField(panel, "Gap Size:", 0.06, ref y, "GapSize");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddNumericField(Panel panel, string label, double defaultValue,
|
||||
private void AddNumericField(Panel panel, string label, double defaultValue,
|
||||
ref int y, string tag)
|
||||
{
|
||||
panel.Controls.Add(new Label
|
||||
{
|
||||
Text = label,
|
||||
Location = new System.Drawing.Point(0, y + 3),
|
||||
Location = new Point(0, y + 3),
|
||||
AutoSize = true
|
||||
});
|
||||
|
||||
panel.Controls.Add(new NumericUpDown
|
||||
{
|
||||
Location = new System.Drawing.Point(130, y),
|
||||
Size = new System.Drawing.Size(120, 22),
|
||||
DecimalPlaces = 4,
|
||||
Increment = 0.0625m,
|
||||
Minimum = 0,
|
||||
Maximum = 9999,
|
||||
Value = (decimal)defaultValue,
|
||||
Tag = tag
|
||||
});
|
||||
var nud = CreateNumeric(130, y, defaultValue, 0.0625);
|
||||
nud.Tag = tag;
|
||||
nud.ValueChanged += (s, e) => OnParametersChanged();
|
||||
panel.Controls.Add(nud);
|
||||
|
||||
y += 30;
|
||||
}
|
||||
|
||||
private void LoadFromParameters(CuttingParameters p)
|
||||
private static NumericUpDown CreateNumeric(int x, int y, double defaultValue, double increment)
|
||||
{
|
||||
LoadLeadIn(cboExternalLeadIn, pnlExternalLeadIn, p.ExternalLeadIn);
|
||||
LoadLeadOut(cboExternalLeadOut, pnlExternalLeadOut, p.ExternalLeadOut);
|
||||
|
||||
LoadLeadIn(cboInternalLeadIn, pnlInternalLeadIn, p.InternalLeadIn);
|
||||
LoadLeadOut(cboInternalLeadOut, pnlInternalLeadOut, p.InternalLeadOut);
|
||||
|
||||
LoadLeadIn(cboArcCircleLeadIn, pnlArcCircleLeadIn, p.ArcCircleLeadIn);
|
||||
LoadLeadOut(cboArcCircleLeadOut, pnlArcCircleLeadOut, p.ArcCircleLeadOut);
|
||||
|
||||
chkTabsEnabled.Checked = p.TabsEnabled;
|
||||
if (p.TabConfig != null)
|
||||
nudTabWidth.Value = (decimal)p.TabConfig.Size;
|
||||
|
||||
nudPierceClearance.Value = (decimal)p.PierceClearance;
|
||||
return new NumericUpDown
|
||||
{
|
||||
Location = new Point(x, y),
|
||||
Size = new Size(120, 22),
|
||||
DecimalPlaces = 4,
|
||||
Increment = (decimal)increment,
|
||||
Minimum = 0,
|
||||
Maximum = 9999,
|
||||
Value = (decimal)defaultValue
|
||||
};
|
||||
}
|
||||
|
||||
private static void LoadLeadIn(ComboBox combo, Panel panel, LeadIn leadIn)
|
||||
@@ -435,88 +523,61 @@ namespace OpenNest.Forms
|
||||
}
|
||||
}
|
||||
|
||||
public CuttingParameters BuildParameters()
|
||||
{
|
||||
var p = new CuttingParameters
|
||||
{
|
||||
ExternalLeadIn = BuildLeadIn(cboExternalLeadIn, pnlExternalLeadIn),
|
||||
ExternalLeadOut = BuildLeadOut(cboExternalLeadOut, pnlExternalLeadOut),
|
||||
InternalLeadIn = BuildLeadIn(cboInternalLeadIn, pnlInternalLeadIn),
|
||||
InternalLeadOut = BuildLeadOut(cboInternalLeadOut, pnlInternalLeadOut),
|
||||
ArcCircleLeadIn = BuildLeadIn(cboArcCircleLeadIn, pnlArcCircleLeadIn),
|
||||
ArcCircleLeadOut = BuildLeadOut(cboArcCircleLeadOut, pnlArcCircleLeadOut),
|
||||
TabsEnabled = chkTabsEnabled.Checked,
|
||||
TabConfig = new NormalTab { Size = (double)nudTabWidth.Value },
|
||||
PierceClearance = (double)nudPierceClearance.Value
|
||||
};
|
||||
return p;
|
||||
}
|
||||
|
||||
private static LeadIn BuildLeadIn(ComboBox combo, Panel panel)
|
||||
{
|
||||
switch (combo.SelectedIndex)
|
||||
return combo.SelectedIndex switch
|
||||
{
|
||||
case 1:
|
||||
return new LineLeadIn
|
||||
{
|
||||
Length = GetParam(panel, "Length", 0.25),
|
||||
ApproachAngle = GetParam(panel, "ApproachAngle", 90)
|
||||
};
|
||||
case 2:
|
||||
return new ArcLeadIn
|
||||
{
|
||||
Radius = GetParam(panel, "Radius", 0.25)
|
||||
};
|
||||
case 3:
|
||||
return new LineArcLeadIn
|
||||
{
|
||||
LineLength = GetParam(panel, "LineLength", 0.25),
|
||||
ArcRadius = GetParam(panel, "ArcRadius", 0.125),
|
||||
ApproachAngle = GetParam(panel, "ApproachAngle", 135)
|
||||
};
|
||||
case 4:
|
||||
return new CleanHoleLeadIn
|
||||
{
|
||||
LineLength = GetParam(panel, "LineLength", 0.25),
|
||||
ArcRadius = GetParam(panel, "ArcRadius", 0.125),
|
||||
Kerf = GetParam(panel, "Kerf", 0.06)
|
||||
};
|
||||
case 5:
|
||||
return new LineLineLeadIn
|
||||
{
|
||||
Length1 = GetParam(panel, "Length1", 0.25),
|
||||
ApproachAngle1 = GetParam(panel, "Angle1", 90),
|
||||
Length2 = GetParam(panel, "Length2", 0.25),
|
||||
ApproachAngle2 = GetParam(panel, "Angle2", 90)
|
||||
};
|
||||
default:
|
||||
return new NoLeadIn();
|
||||
}
|
||||
1 => new LineLeadIn
|
||||
{
|
||||
Length = GetParam(panel, "Length", 0.25),
|
||||
ApproachAngle = GetParam(panel, "ApproachAngle", 90)
|
||||
},
|
||||
2 => new ArcLeadIn
|
||||
{
|
||||
Radius = GetParam(panel, "Radius", 0.25)
|
||||
},
|
||||
3 => new LineArcLeadIn
|
||||
{
|
||||
LineLength = GetParam(panel, "LineLength", 0.25),
|
||||
ArcRadius = GetParam(panel, "ArcRadius", 0.125),
|
||||
ApproachAngle = GetParam(panel, "ApproachAngle", 135)
|
||||
},
|
||||
4 => new CleanHoleLeadIn
|
||||
{
|
||||
LineLength = GetParam(panel, "LineLength", 0.25),
|
||||
ArcRadius = GetParam(panel, "ArcRadius", 0.125),
|
||||
Kerf = GetParam(panel, "Kerf", 0.06)
|
||||
},
|
||||
5 => new LineLineLeadIn
|
||||
{
|
||||
Length1 = GetParam(panel, "Length1", 0.25),
|
||||
ApproachAngle1 = GetParam(panel, "Angle1", 90),
|
||||
Length2 = GetParam(panel, "Length2", 0.25),
|
||||
ApproachAngle2 = GetParam(panel, "Angle2", 90)
|
||||
},
|
||||
_ => new NoLeadIn()
|
||||
};
|
||||
}
|
||||
|
||||
private static LeadOut BuildLeadOut(ComboBox combo, Panel panel)
|
||||
{
|
||||
switch (combo.SelectedIndex)
|
||||
return combo.SelectedIndex switch
|
||||
{
|
||||
case 1:
|
||||
return new LineLeadOut
|
||||
{
|
||||
Length = GetParam(panel, "Length", 0.25),
|
||||
ApproachAngle = GetParam(panel, "ApproachAngle", 90)
|
||||
};
|
||||
case 2:
|
||||
return new ArcLeadOut
|
||||
{
|
||||
Radius = GetParam(panel, "Radius", 0.25)
|
||||
};
|
||||
case 3:
|
||||
return new MicrotabLeadOut
|
||||
{
|
||||
GapSize = GetParam(panel, "GapSize", 0.06)
|
||||
};
|
||||
default:
|
||||
return new NoLeadOut();
|
||||
}
|
||||
1 => new LineLeadOut
|
||||
{
|
||||
Length = GetParam(panel, "Length", 0.25),
|
||||
ApproachAngle = GetParam(panel, "ApproachAngle", 90)
|
||||
},
|
||||
2 => new ArcLeadOut
|
||||
{
|
||||
Radius = GetParam(panel, "Radius", 0.25)
|
||||
},
|
||||
3 => new MicrotabLeadOut
|
||||
{
|
||||
GapSize = GetParam(panel, "GapSize", 0.06)
|
||||
},
|
||||
_ => new NoLeadOut()
|
||||
};
|
||||
}
|
||||
|
||||
private static void SetParam(Panel panel, string tag, double value)
|
||||
+5
-19
@@ -28,9 +28,8 @@ namespace OpenNest.Controls
|
||||
rightSplit = new System.Windows.Forms.SplitContainer();
|
||||
preview = new EntityView();
|
||||
editorPanel = new System.Windows.Forms.Panel();
|
||||
gcodeEditor = new System.Windows.Forms.TextBox();
|
||||
gcodeEditor = new System.Windows.Forms.RichTextBox();
|
||||
editorToolbar = new System.Windows.Forms.Panel();
|
||||
applyButton = new System.Windows.Forms.Button();
|
||||
lblGcode = new System.Windows.Forms.Label();
|
||||
((System.ComponentModel.ISupportInitialize)mainSplit).BeginInit();
|
||||
mainSplit.Panel1.SuspendLayout();
|
||||
@@ -186,9 +185,9 @@ namespace OpenNest.Controls
|
||||
gcodeEditor.Font = new System.Drawing.Font("Consolas", 10F);
|
||||
gcodeEditor.ForeColor = System.Drawing.Color.FromArgb(180, 200, 180);
|
||||
gcodeEditor.Location = new System.Drawing.Point(0, 30);
|
||||
gcodeEditor.Multiline = true;
|
||||
gcodeEditor.Name = "gcodeEditor";
|
||||
gcodeEditor.ScrollBars = System.Windows.Forms.ScrollBars.Both;
|
||||
gcodeEditor.ReadOnly = true;
|
||||
gcodeEditor.ScrollBars = System.Windows.Forms.RichTextBoxScrollBars.Both;
|
||||
gcodeEditor.Size = new System.Drawing.Size(260, 470);
|
||||
gcodeEditor.TabIndex = 1;
|
||||
gcodeEditor.WordWrap = false;
|
||||
@@ -196,7 +195,6 @@ namespace OpenNest.Controls
|
||||
// editorToolbar
|
||||
//
|
||||
editorToolbar.BackColor = System.Drawing.Color.FromArgb(245, 245, 245);
|
||||
editorToolbar.Controls.Add(applyButton);
|
||||
editorToolbar.Controls.Add(lblGcode);
|
||||
editorToolbar.Dock = System.Windows.Forms.DockStyle.Top;
|
||||
editorToolbar.Location = new System.Drawing.Point(0, 0);
|
||||
@@ -204,18 +202,7 @@ namespace OpenNest.Controls
|
||||
editorToolbar.Padding = new System.Windows.Forms.Padding(6, 4, 6, 4);
|
||||
editorToolbar.Size = new System.Drawing.Size(260, 30);
|
||||
editorToolbar.TabIndex = 0;
|
||||
//
|
||||
// applyButton
|
||||
//
|
||||
applyButton.Dock = System.Windows.Forms.DockStyle.Right;
|
||||
applyButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
|
||||
applyButton.Font = new System.Drawing.Font("Segoe UI", 9F);
|
||||
applyButton.Location = new System.Drawing.Point(184, 4);
|
||||
applyButton.Name = "applyButton";
|
||||
applyButton.Size = new System.Drawing.Size(70, 22);
|
||||
applyButton.TabIndex = 1;
|
||||
applyButton.Text = "Apply";
|
||||
//
|
||||
//
|
||||
// lblGcode
|
||||
//
|
||||
lblGcode.AutoSize = true;
|
||||
@@ -263,8 +250,7 @@ namespace OpenNest.Controls
|
||||
private System.Windows.Forms.Panel editorPanel;
|
||||
private System.Windows.Forms.Panel editorToolbar;
|
||||
private System.Windows.Forms.Label lblGcode;
|
||||
private System.Windows.Forms.Button applyButton;
|
||||
private System.Windows.Forms.TextBox gcodeEditor;
|
||||
private System.Windows.Forms.RichTextBox gcodeEditor;
|
||||
private System.Windows.Forms.ContextMenuStrip contourMenu;
|
||||
private System.Windows.Forms.ToolStripMenuItem menuReverse;
|
||||
private System.Windows.Forms.ToolStripMenuItem menuMoveUp;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.IO;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace OpenNest.Controls
|
||||
@@ -29,7 +29,6 @@ namespace OpenNest.Controls
|
||||
menuMoveDown.Click += OnMoveDownClicked;
|
||||
menuSequence.Click += OnSequenceClicked;
|
||||
contourMenu.Opening += OnContourMenuOpening;
|
||||
applyButton.Click += OnApplyClicked;
|
||||
preview.PaintOverlay = OnPreviewPaintOverlay;
|
||||
}
|
||||
|
||||
@@ -92,34 +91,49 @@ namespace OpenNest.Controls
|
||||
|
||||
private void UpdateGcodeText()
|
||||
{
|
||||
gcodeEditor.Text = Program != null ? FormatProgram(Program) : string.Empty;
|
||||
var text = Program != null ? FormatProgram(Program, contours) : string.Empty;
|
||||
gcodeEditor.Text = text;
|
||||
ApplyHighlighting();
|
||||
}
|
||||
|
||||
private static string FormatProgram(Program pgm)
|
||||
private static string FormatProgram(Program pgm, List<ContourInfo> contours)
|
||||
{
|
||||
var sb = new System.Text.StringBuilder();
|
||||
sb.AppendLine(pgm.Mode == Mode.Absolute ? "G90" : "G91");
|
||||
|
||||
var lastWasRapid = false;
|
||||
foreach (var code in pgm.Codes)
|
||||
var codeIndex = 0;
|
||||
var codes = pgm.Codes;
|
||||
|
||||
foreach (var contour in contours)
|
||||
{
|
||||
if (code is RapidMove rapid)
|
||||
var sub = ConvertGeometry.ToProgram(contour.Shape);
|
||||
if (sub == null) continue;
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"; {contour.Label} ({contour.DirectionLabel})");
|
||||
|
||||
var lastWasRapid = false;
|
||||
for (var i = 0; i < sub.Length && codeIndex < codes.Count; i++, codeIndex++)
|
||||
{
|
||||
if (!lastWasRapid && sb.Length > 0)
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"G00 X{FormatCoord(rapid.EndPoint.X)} Y{FormatCoord(rapid.EndPoint.Y)}");
|
||||
lastWasRapid = true;
|
||||
}
|
||||
else if (code is ArcMove arc)
|
||||
{
|
||||
var g = arc.Rotation == RotationType.CW ? "G02" : "G03";
|
||||
sb.AppendLine($"{g} X{FormatCoord(arc.EndPoint.X)} Y{FormatCoord(arc.EndPoint.Y)} I{FormatCoord(arc.CenterPoint.X)} J{FormatCoord(arc.CenterPoint.Y)}");
|
||||
lastWasRapid = false;
|
||||
}
|
||||
else if (code is LinearMove linear)
|
||||
{
|
||||
sb.AppendLine($"G01 X{FormatCoord(linear.EndPoint.X)} Y{FormatCoord(linear.EndPoint.Y)}");
|
||||
lastWasRapid = false;
|
||||
var code = codes[codeIndex];
|
||||
if (code is RapidMove rapid)
|
||||
{
|
||||
if (!lastWasRapid)
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"G00 X{FormatCoord(rapid.EndPoint.X)} Y{FormatCoord(rapid.EndPoint.Y)}");
|
||||
lastWasRapid = true;
|
||||
}
|
||||
else if (code is ArcMove arc)
|
||||
{
|
||||
var g = arc.Rotation == RotationType.CW ? "G02" : "G03";
|
||||
sb.AppendLine($"{g} X{FormatCoord(arc.EndPoint.X)} Y{FormatCoord(arc.EndPoint.Y)} I{FormatCoord(arc.CenterPoint.X)} J{FormatCoord(arc.CenterPoint.Y)}");
|
||||
lastWasRapid = false;
|
||||
}
|
||||
else if (code is LinearMove linear)
|
||||
{
|
||||
sb.AppendLine($"G01 X{FormatCoord(linear.EndPoint.X)} Y{FormatCoord(linear.EndPoint.Y)}");
|
||||
lastWasRapid = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +145,45 @@ namespace OpenNest.Controls
|
||||
return System.Math.Round(value, 4).ToString("0.####", System.Globalization.CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
private void ApplyHighlighting()
|
||||
{
|
||||
var text = gcodeEditor.Text;
|
||||
if (string.IsNullOrEmpty(text)) return;
|
||||
|
||||
gcodeEditor.SuspendLayout();
|
||||
|
||||
var rapidColor = Color.FromArgb(230, 180, 80);
|
||||
var linearColor = Color.FromArgb(130, 200, 140);
|
||||
var arcColor = Color.FromArgb(120, 160, 255);
|
||||
var commentColor = Color.FromArgb(120, 120, 140);
|
||||
var modeColor = Color.FromArgb(200, 140, 220);
|
||||
var coordColor = Color.FromArgb(180, 200, 180);
|
||||
|
||||
gcodeEditor.SelectAll();
|
||||
gcodeEditor.SelectionColor = coordColor;
|
||||
|
||||
var rules = new (Regex pattern, Color color)[]
|
||||
{
|
||||
(new Regex(@"^;.*$", RegexOptions.Multiline), commentColor),
|
||||
(new Regex(@"^G9[01]\b", RegexOptions.Multiline), modeColor),
|
||||
(new Regex(@"^G00\b", RegexOptions.Multiline), rapidColor),
|
||||
(new Regex(@"^G01\b", RegexOptions.Multiline), linearColor),
|
||||
(new Regex(@"^G0[23]\b", RegexOptions.Multiline), arcColor),
|
||||
};
|
||||
|
||||
foreach (var (pattern, color) in rules)
|
||||
{
|
||||
foreach (Match match in pattern.Matches(text))
|
||||
{
|
||||
gcodeEditor.Select(match.Index, match.Length);
|
||||
gcodeEditor.SelectionColor = color;
|
||||
}
|
||||
}
|
||||
|
||||
gcodeEditor.Select(0, 0);
|
||||
gcodeEditor.ResumeLayout();
|
||||
}
|
||||
|
||||
private void RefreshPreview()
|
||||
{
|
||||
preview.ClearPenCache();
|
||||
@@ -418,50 +471,5 @@ namespace OpenNest.Controls
|
||||
}
|
||||
}
|
||||
|
||||
private void OnApplyClicked(object sender, EventArgs e)
|
||||
{
|
||||
var text = gcodeEditor.Text;
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
MessageBox.Show("G-code is empty.", "Apply", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var stream = new System.IO.MemoryStream(System.Text.Encoding.UTF8.GetBytes(text));
|
||||
var reader = new ProgramReader(stream);
|
||||
var parsed = reader.Read();
|
||||
|
||||
if (parsed == null || parsed.Length == 0)
|
||||
{
|
||||
MessageBox.Show("No valid G-code found.", "Apply", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
// Rebuild shapes from the parsed program
|
||||
var entities = ConvertProgram.ToGeometry(parsed);
|
||||
var shapes = ShapeBuilder.GetShapes(entities);
|
||||
|
||||
if (shapes.Count == 0)
|
||||
{
|
||||
MessageBox.Show("No contours found in parsed G-code.", "Apply", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
contours = ContourInfo.Classify(shapes);
|
||||
Program = parsed;
|
||||
isDirty = true;
|
||||
|
||||
PopulateContourList();
|
||||
RefreshPreview();
|
||||
ProgramChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"Error parsing G-code: {ex.Message}", "Apply",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-156
@@ -1,156 +0,0 @@
|
||||
namespace OpenNest.Forms
|
||||
{
|
||||
partial class CuttingParametersForm
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// Clean up any resources being used.
|
||||
/// </summary>
|
||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows Form Designer generated code
|
||||
|
||||
/// <summary>
|
||||
/// Required method for Designer support - do not modify
|
||||
/// the contents of this method with the code editor.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this.tabControl = new System.Windows.Forms.TabControl();
|
||||
this.tabExternal = new System.Windows.Forms.TabPage();
|
||||
this.tabInternal = new System.Windows.Forms.TabPage();
|
||||
this.tabArcCircle = new System.Windows.Forms.TabPage();
|
||||
this.acceptButton = new System.Windows.Forms.Button();
|
||||
this.cancelButton = new System.Windows.Forms.Button();
|
||||
this.bottomPanel = new OpenNest.Controls.BottomPanel();
|
||||
this.tabControl.SuspendLayout();
|
||||
this.bottomPanel.SuspendLayout();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// tabControl
|
||||
//
|
||||
this.tabControl.Controls.Add(this.tabExternal);
|
||||
this.tabControl.Controls.Add(this.tabInternal);
|
||||
this.tabControl.Controls.Add(this.tabArcCircle);
|
||||
this.tabControl.Dock = System.Windows.Forms.DockStyle.Top;
|
||||
this.tabControl.Location = new System.Drawing.Point(0, 0);
|
||||
this.tabControl.Margin = new System.Windows.Forms.Padding(4);
|
||||
this.tabControl.Name = "tabControl";
|
||||
this.tabControl.SelectedIndex = 0;
|
||||
this.tabControl.Size = new System.Drawing.Size(380, 348);
|
||||
this.tabControl.TabIndex = 0;
|
||||
//
|
||||
// tabExternal
|
||||
//
|
||||
this.tabExternal.Location = new System.Drawing.Point(4, 25);
|
||||
this.tabExternal.Margin = new System.Windows.Forms.Padding(4);
|
||||
this.tabExternal.Name = "tabExternal";
|
||||
this.tabExternal.Padding = new System.Windows.Forms.Padding(8);
|
||||
this.tabExternal.Size = new System.Drawing.Size(372, 319);
|
||||
this.tabExternal.TabIndex = 0;
|
||||
this.tabExternal.Text = "External";
|
||||
this.tabExternal.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// tabInternal
|
||||
//
|
||||
this.tabInternal.Location = new System.Drawing.Point(4, 25);
|
||||
this.tabInternal.Margin = new System.Windows.Forms.Padding(4);
|
||||
this.tabInternal.Name = "tabInternal";
|
||||
this.tabInternal.Padding = new System.Windows.Forms.Padding(8);
|
||||
this.tabInternal.Size = new System.Drawing.Size(372, 319);
|
||||
this.tabInternal.TabIndex = 1;
|
||||
this.tabInternal.Text = "Internal";
|
||||
this.tabInternal.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// tabArcCircle
|
||||
//
|
||||
this.tabArcCircle.Location = new System.Drawing.Point(4, 25);
|
||||
this.tabArcCircle.Margin = new System.Windows.Forms.Padding(4);
|
||||
this.tabArcCircle.Name = "tabArcCircle";
|
||||
this.tabArcCircle.Padding = new System.Windows.Forms.Padding(8);
|
||||
this.tabArcCircle.Size = new System.Drawing.Size(372, 319);
|
||||
this.tabArcCircle.TabIndex = 2;
|
||||
this.tabArcCircle.Text = "Arc / Circle";
|
||||
this.tabArcCircle.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// acceptButton
|
||||
//
|
||||
this.acceptButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.acceptButton.DialogResult = System.Windows.Forms.DialogResult.OK;
|
||||
this.acceptButton.Location = new System.Drawing.Point(165, 11);
|
||||
this.acceptButton.Margin = new System.Windows.Forms.Padding(4);
|
||||
this.acceptButton.Name = "acceptButton";
|
||||
this.acceptButton.Size = new System.Drawing.Size(90, 28);
|
||||
this.acceptButton.TabIndex = 8;
|
||||
this.acceptButton.Text = "OK";
|
||||
this.acceptButton.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// cancelButton
|
||||
//
|
||||
this.cancelButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.cancelButton.DialogResult = System.Windows.Forms.DialogResult.Cancel;
|
||||
this.cancelButton.Location = new System.Drawing.Point(263, 11);
|
||||
this.cancelButton.Margin = new System.Windows.Forms.Padding(4);
|
||||
this.cancelButton.Name = "cancelButton";
|
||||
this.cancelButton.Size = new System.Drawing.Size(90, 28);
|
||||
this.cancelButton.TabIndex = 9;
|
||||
this.cancelButton.Text = "Cancel";
|
||||
this.cancelButton.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// bottomPanel
|
||||
//
|
||||
this.bottomPanel.Controls.Add(this.acceptButton);
|
||||
this.bottomPanel.Controls.Add(this.cancelButton);
|
||||
this.bottomPanel.Dock = System.Windows.Forms.DockStyle.Bottom;
|
||||
this.bottomPanel.Location = new System.Drawing.Point(0, 466);
|
||||
this.bottomPanel.Name = "bottomPanel";
|
||||
this.bottomPanel.Size = new System.Drawing.Size(380, 50);
|
||||
this.bottomPanel.TabIndex = 1;
|
||||
//
|
||||
// CuttingParametersForm
|
||||
//
|
||||
this.AcceptButton = this.acceptButton;
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.CancelButton = this.cancelButton;
|
||||
this.ClientSize = new System.Drawing.Size(380, 516);
|
||||
this.Controls.Add(this.tabControl);
|
||||
this.Controls.Add(this.bottomPanel);
|
||||
this.Font = new System.Drawing.Font("Microsoft Sans Serif", 9.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
|
||||
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
|
||||
this.Margin = new System.Windows.Forms.Padding(4);
|
||||
this.MaximizeBox = false;
|
||||
this.MinimizeBox = false;
|
||||
this.Name = "CuttingParametersForm";
|
||||
this.ShowIcon = false;
|
||||
this.ShowInTaskbar = false;
|
||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||
this.Text = "Cutting Parameters";
|
||||
this.tabControl.ResumeLayout(false);
|
||||
this.bottomPanel.ResumeLayout(false);
|
||||
this.ResumeLayout(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private System.Windows.Forms.TabControl tabControl;
|
||||
private System.Windows.Forms.TabPage tabExternal;
|
||||
private System.Windows.Forms.TabPage tabInternal;
|
||||
private System.Windows.Forms.TabPage tabArcCircle;
|
||||
private Controls.BottomPanel bottomPanel;
|
||||
private System.Windows.Forms.Button acceptButton;
|
||||
private System.Windows.Forms.Button cancelButton;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ using System.Text.Json;
|
||||
|
||||
namespace OpenNest.Forms
|
||||
{
|
||||
internal static class CuttingParametersSerializer
|
||||
public static class CuttingParametersSerializer
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
@@ -23,7 +23,9 @@ namespace OpenNest.Forms
|
||||
ArcCircleLeadOut = ToLeadOutDto(p.ArcCircleLeadOut),
|
||||
TabsEnabled = p.TabsEnabled,
|
||||
TabWidth = p.TabConfig?.Size ?? 0.25,
|
||||
PierceClearance = p.PierceClearance
|
||||
PierceClearance = p.PierceClearance,
|
||||
AutoTabMinSize = p.AutoTabMinSize,
|
||||
AutoTabMaxSize = p.AutoTabMaxSize
|
||||
};
|
||||
return JsonSerializer.Serialize(dto, JsonOptions);
|
||||
}
|
||||
@@ -44,7 +46,9 @@ namespace OpenNest.Forms
|
||||
ArcCircleLeadOut = FromLeadOutDto(dto.ArcCircleLeadOut),
|
||||
TabsEnabled = dto.TabsEnabled,
|
||||
TabConfig = new NormalTab { Size = dto.TabWidth },
|
||||
PierceClearance = dto.PierceClearance
|
||||
PierceClearance = dto.PierceClearance,
|
||||
AutoTabMinSize = dto.AutoTabMinSize,
|
||||
AutoTabMaxSize = dto.AutoTabMaxSize
|
||||
};
|
||||
}
|
||||
|
||||
@@ -109,6 +113,8 @@ namespace OpenNest.Forms
|
||||
public bool TabsEnabled { get; set; }
|
||||
public double TabWidth { get; set; }
|
||||
public double PierceClearance { get; set; }
|
||||
public double AutoTabMinSize { get; set; }
|
||||
public double AutoTabMaxSize { get; set; }
|
||||
}
|
||||
|
||||
private class LeadInDto
|
||||
|
||||
@@ -37,6 +37,9 @@ namespace OpenNest.Forms
|
||||
private Button btnNextPlate;
|
||||
private Button btnLastPlate;
|
||||
|
||||
private SplitContainer viewSplitContainer;
|
||||
private Panel sidePanel;
|
||||
|
||||
/// <summary>
|
||||
/// Used to distinguish between single/double click on drawing within drawinglistbox.
|
||||
/// If double click, this is set to false so the single click action won't be triggered.
|
||||
@@ -53,8 +56,9 @@ namespace OpenNest.Forms
|
||||
|
||||
InitializeComponent();
|
||||
CreatePlateHeader();
|
||||
CreateSidePanel();
|
||||
|
||||
splitContainer.Panel2.Controls.Add(PlateView);
|
||||
splitContainer.Panel2.Controls.Add(viewSplitContainer);
|
||||
splitContainer.Panel2.Controls.Add(plateHeaderPanel);
|
||||
|
||||
var renderer = new ToolStripRenderer(ToolbarTheme.Toolbar);
|
||||
@@ -146,6 +150,43 @@ namespace OpenNest.Forms
|
||||
navPanel.Top = (plateHeaderPanel.Height - navPanel.Height) / 2;
|
||||
}
|
||||
|
||||
private void CreateSidePanel()
|
||||
{
|
||||
sidePanel = new Panel
|
||||
{
|
||||
Dock = DockStyle.Fill,
|
||||
AutoScroll = true,
|
||||
BackColor = Color.White
|
||||
};
|
||||
|
||||
viewSplitContainer = new SplitContainer
|
||||
{
|
||||
Dock = DockStyle.Fill,
|
||||
Orientation = Orientation.Vertical,
|
||||
FixedPanel = FixedPanel.Panel2,
|
||||
Panel2MinSize = 0
|
||||
};
|
||||
|
||||
viewSplitContainer.Panel1.Controls.Add(PlateView);
|
||||
viewSplitContainer.Panel2.Controls.Add(sidePanel);
|
||||
viewSplitContainer.Panel2Collapsed = true;
|
||||
}
|
||||
|
||||
public void ShowSidePanel(Control content, int width = 390)
|
||||
{
|
||||
sidePanel.Controls.Clear();
|
||||
content.Dock = DockStyle.Fill;
|
||||
sidePanel.Controls.Add(content);
|
||||
viewSplitContainer.SplitterDistance = viewSplitContainer.Width - width;
|
||||
viewSplitContainer.Panel2Collapsed = false;
|
||||
}
|
||||
|
||||
public void HideSidePanel()
|
||||
{
|
||||
viewSplitContainer.Panel2Collapsed = true;
|
||||
sidePanel.Controls.Clear();
|
||||
}
|
||||
|
||||
private static Button CreateNavButton(System.Drawing.Image image)
|
||||
{
|
||||
return new Button
|
||||
@@ -725,15 +766,19 @@ namespace OpenNest.Forms
|
||||
|
||||
var plate = PlateView.Plate;
|
||||
|
||||
using var form = new CuttingParametersForm();
|
||||
if (plate.CuttingParameters != null)
|
||||
form.Parameters = plate.CuttingParameters;
|
||||
|
||||
if (form.ShowDialog(this) != DialogResult.OK)
|
||||
return;
|
||||
|
||||
var parameters = form.BuildParameters();
|
||||
plate.CuttingParameters = parameters;
|
||||
if (plate.CuttingParameters == null)
|
||||
{
|
||||
var json = Properties.Settings.Default.CuttingParametersJson;
|
||||
if (!string.IsNullOrEmpty(json))
|
||||
{
|
||||
try { plate.CuttingParameters = CuttingParametersSerializer.Deserialize(json); }
|
||||
catch { plate.CuttingParameters = new CuttingParameters(); }
|
||||
}
|
||||
else
|
||||
{
|
||||
plate.CuttingParameters = new CuttingParameters();
|
||||
}
|
||||
}
|
||||
|
||||
var assigner = new LeadInAssigner
|
||||
{
|
||||
@@ -784,11 +829,18 @@ namespace OpenNest.Forms
|
||||
if (Nest == null)
|
||||
return;
|
||||
|
||||
using var form = new CuttingParametersForm();
|
||||
if (form.ShowDialog(this) != DialogResult.OK)
|
||||
return;
|
||||
CuttingParameters parameters;
|
||||
var json = Properties.Settings.Default.CuttingParametersJson;
|
||||
if (!string.IsNullOrEmpty(json))
|
||||
{
|
||||
try { parameters = CuttingParametersSerializer.Deserialize(json); }
|
||||
catch { parameters = new CuttingParameters(); }
|
||||
}
|
||||
else
|
||||
{
|
||||
parameters = new CuttingParameters();
|
||||
}
|
||||
|
||||
var parameters = form.BuildParameters();
|
||||
var assigner = new LeadInAssigner
|
||||
{
|
||||
Sequencer = new LeftSideSequencer()
|
||||
@@ -835,14 +887,19 @@ namespace OpenNest.Forms
|
||||
|
||||
var plate = PlateView.Plate;
|
||||
|
||||
// Ensure cutting parameters are configured
|
||||
// If no cutting parameters exist, initialize from saved settings or defaults
|
||||
if (plate.CuttingParameters == null)
|
||||
{
|
||||
using var form = new CuttingParametersForm();
|
||||
if (form.ShowDialog(this) != DialogResult.OK)
|
||||
return;
|
||||
|
||||
plate.CuttingParameters = form.BuildParameters();
|
||||
var json = Properties.Settings.Default.CuttingParametersJson;
|
||||
if (!string.IsNullOrEmpty(json))
|
||||
{
|
||||
try { plate.CuttingParameters = CuttingParametersSerializer.Deserialize(json); }
|
||||
catch { plate.CuttingParameters = new CuttingParameters(); }
|
||||
}
|
||||
else
|
||||
{
|
||||
plate.CuttingParameters = new CuttingParameters();
|
||||
}
|
||||
}
|
||||
|
||||
PlateView.SetAction(typeof(Actions.ActionLeadIn));
|
||||
|
||||
@@ -22,6 +22,7 @@ OpenNest takes your part drawings, lets you define your sheet (plate) sizes, and
|
||||
- **Lead-In/Lead-Out & Tabs** — Configurable approach paths, exit paths, and holding tabs for CNC cutting, with snap-to-endpoint/midpoint placement
|
||||
- **Contour & Program Editing** — Inline G-code editor with contour reordering, direction arrows, and cut direction reversal
|
||||
- **G-code Output** — Post-process nested layouts to G-code via plugin post-processors
|
||||
- **User-Defined Variables** — Define named variables in G-code (`diameter = 0.3`) referenced with `$name` syntax; Cincinnati post emits numbered machine variables (`#200`) so operators can adjust values at the control
|
||||
- **Built-in Shapes** — 12 parametric shapes (circles, rectangles, L-shapes, T-shapes, flanges, etc.) for quick testing or simple parts
|
||||
- **Interactive Editing** — Zoom, pan, select, clone, push, and manually arrange parts on the plate view
|
||||
- **Pluggable Engine Architecture** — Swap between built-in nesting engines or load custom engines from plugin DLLs
|
||||
@@ -212,7 +213,7 @@ Custom post-processors implement the `IPostProcessor` interface and are auto-dis
|
||||
Nest files (`.nest`) are ZIP archives containing:
|
||||
|
||||
- `nest.json` — JSON metadata: nest info, plate defaults, drawings (with bend data), and plates (with parts and cut-offs)
|
||||
- `programs/program-N` — G-code text for each drawing's cut program
|
||||
- `programs/program-N` — G-code text for each drawing's cut program (may include variable definitions and `$name` references)
|
||||
- `bestfits/bestfit-N` — Cached best-fit pair evaluation results (optional)
|
||||
|
||||
## Roadmap
|
||||
|
||||
Reference in New Issue
Block a user