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