Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 833abfe72e | |||
| 379000bbd8 | |||
| 5936272ce4 | |||
| da8e7e6fd3 | |||
| 53d24ddaf1 | |||
| 8efdc8720c | |||
| ca8a0942ab | |||
| 8c3659a439 | |||
| 95a0815484 | |||
| e9caa9b8eb |
@@ -1,4 +1,4 @@
|
|||||||
namespace OpenNest.Posts.Cincinnati
|
namespace OpenNest.CNC
|
||||||
{
|
{
|
||||||
public sealed class ProgramVariable
|
public sealed class ProgramVariable
|
||||||
{
|
{
|
||||||
+1
-1
@@ -2,7 +2,7 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace OpenNest.Posts.Cincinnati
|
namespace OpenNest.CNC
|
||||||
{
|
{
|
||||||
public sealed class ProgramVariableManager
|
public sealed class ProgramVariableManager
|
||||||
{
|
{
|
||||||
+22
-2
@@ -129,9 +129,11 @@ namespace OpenNest
|
|||||||
private List<(double Start, double End)> IntersectPerimeter(
|
private List<(double Start, double End)> IntersectPerimeter(
|
||||||
Entity perimeter, double cutPosition, double lineStart, double lineEnd, double clearance)
|
Entity perimeter, double cutPosition, double lineStart, double lineEnd, double clearance)
|
||||||
{
|
{
|
||||||
|
var target = OffsetOutward(perimeter, clearance) ?? perimeter;
|
||||||
|
var usedOffset = target != perimeter;
|
||||||
var cutLine = new Line(MakePoint(cutPosition, lineStart), MakePoint(cutPosition, lineEnd));
|
var cutLine = new Line(MakePoint(cutPosition, lineStart), MakePoint(cutPosition, lineEnd));
|
||||||
|
|
||||||
if (!perimeter.Intersects(cutLine, out var pts) || pts.Count < 2)
|
if (!target.Intersects(cutLine, out var pts) || pts.Count < 2)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
var coords = pts
|
var coords = pts
|
||||||
@@ -142,13 +144,31 @@ namespace OpenNest
|
|||||||
if (coords.Count % 2 != 0)
|
if (coords.Count % 2 != 0)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
|
var padding = usedOffset ? 0 : clearance;
|
||||||
var result = new List<(double Start, double End)>();
|
var result = new List<(double Start, double End)>();
|
||||||
for (var i = 0; i < coords.Count; i += 2)
|
for (var i = 0; i < coords.Count; i += 2)
|
||||||
result.Add((coords[i] - clearance, coords[i + 1] + clearance));
|
result.Add((coords[i] - padding, coords[i + 1] + padding));
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Entity OffsetOutward(Entity perimeter, double clearance)
|
||||||
|
{
|
||||||
|
if (clearance <= 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var offset = perimeter.OffsetEntity(clearance, OffsetSide.Left);
|
||||||
|
offset?.UpdateBounds();
|
||||||
|
return offset;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private Vector MakePoint(double cutCoord, double lineCoord) =>
|
private Vector MakePoint(double cutCoord, double lineCoord) =>
|
||||||
Axis == CutOffAxis.Vertical
|
Axis == CutOffAxis.Vertical
|
||||||
? new Vector(cutCoord, lineCoord)
|
? new Vector(cutCoord, lineCoord)
|
||||||
|
|||||||
@@ -317,12 +317,68 @@ namespace OpenNest.Geometry
|
|||||||
|
|
||||||
public override Entity OffsetEntity(double distance, OffsetSide side)
|
public override Entity OffsetEntity(double distance, OffsetSide side)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
if (Vertices.Count < 3)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var isClosed = IsClosed();
|
||||||
|
var count = isClosed ? Vertices.Count - 1 : Vertices.Count;
|
||||||
|
if (count < 3)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var ccw = CalculateArea() > 0;
|
||||||
|
var outward = ccw ? OffsetSide.Left : OffsetSide.Right;
|
||||||
|
var sign = side == outward ? 1.0 : -1.0;
|
||||||
|
var d = distance * sign;
|
||||||
|
|
||||||
|
var normals = new Vector[count];
|
||||||
|
for (var i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
var next = (i + 1) % count;
|
||||||
|
var dx = Vertices[next].X - Vertices[i].X;
|
||||||
|
var dy = Vertices[next].Y - Vertices[i].Y;
|
||||||
|
var len = System.Math.Sqrt(dx * dx + dy * dy);
|
||||||
|
if (len < Tolerance.Epsilon)
|
||||||
|
return null;
|
||||||
|
normals[i] = new Vector(-dy / len * d, dx / len * d);
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = new Polygon();
|
||||||
|
for (var i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
var prev = (i - 1 + count) % count;
|
||||||
|
|
||||||
|
var a1 = new Vector(Vertices[prev].X + normals[prev].X, Vertices[prev].Y + normals[prev].Y);
|
||||||
|
var a2 = new Vector(Vertices[i].X + normals[prev].X, Vertices[i].Y + normals[prev].Y);
|
||||||
|
var b1 = new Vector(Vertices[i].X + normals[i].X, Vertices[i].Y + normals[i].Y);
|
||||||
|
var b2 = new Vector(Vertices[(i + 1) % count].X + normals[i].X, Vertices[(i + 1) % count].Y + normals[i].Y);
|
||||||
|
|
||||||
|
var edgeA = new Line(a1, a2);
|
||||||
|
var edgeB = new Line(b1, b2);
|
||||||
|
|
||||||
|
if (edgeA.Intersects(edgeB, out var pt) && pt.IsValid())
|
||||||
|
result.Vertices.Add(pt);
|
||||||
|
else
|
||||||
|
result.Vertices.Add(new Vector(Vertices[i].X + normals[i].X, Vertices[i].Y + normals[i].Y));
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Close();
|
||||||
|
result.RemoveSelfIntersections();
|
||||||
|
result.UpdateBounds();
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Entity OffsetEntity(double distance, Vector pt)
|
public override Entity OffsetEntity(double distance, Vector pt)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
var left = OffsetEntity(distance, OffsetSide.Left);
|
||||||
|
var right = OffsetEntity(distance, OffsetSide.Right);
|
||||||
|
|
||||||
|
if (left == null) return right;
|
||||||
|
if (right == null) return left;
|
||||||
|
|
||||||
|
var distLeft = left.ClosestPointTo(pt).DistanceTo(pt);
|
||||||
|
var distRight = right.ClosestPointTo(pt).DistanceTo(pt);
|
||||||
|
|
||||||
|
return distLeft > distRight ? left : right;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -0,0 +1,230 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using OpenNest.CNC;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using OpenNest.Math;
|
||||||
|
|
||||||
|
namespace OpenNest.Posts.Cincinnati;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Data class carrying all context needed to emit one Cincinnati-format G-code feature block.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class FeatureContext
|
||||||
|
{
|
||||||
|
public List<ICode> Codes { get; set; } = new();
|
||||||
|
public int FeatureNumber { get; set; }
|
||||||
|
public string PartName { get; set; } = "";
|
||||||
|
public bool IsFirstFeatureOfPart { get; set; }
|
||||||
|
public bool IsLastFeatureOnSheet { get; set; }
|
||||||
|
public bool IsSafetyHeadraise { get; set; }
|
||||||
|
public bool IsExteriorFeature { get; set; }
|
||||||
|
public string LibraryFile { get; set; } = "";
|
||||||
|
public double CutDistance { get; set; }
|
||||||
|
public double SheetDiagonal { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Emits one Cincinnati-format G-code feature block (one contour) to a TextWriter.
|
||||||
|
/// Handles rapid positioning, pierce, kerf compensation, anti-dive, feedrate modal
|
||||||
|
/// suppression, arc I/J conversion (absolute to incremental), and M47 head raise.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CincinnatiFeatureWriter
|
||||||
|
{
|
||||||
|
private readonly CincinnatiPostConfig _config;
|
||||||
|
private readonly CoordinateFormatter _fmt;
|
||||||
|
private readonly SpeedClassifier _speedClassifier;
|
||||||
|
|
||||||
|
public CincinnatiFeatureWriter(CincinnatiPostConfig config)
|
||||||
|
{
|
||||||
|
_config = config;
|
||||||
|
_fmt = new CoordinateFormatter(config.PostedAccuracy);
|
||||||
|
_speedClassifier = new SpeedClassifier();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Writes a complete feature block for the given context.
|
||||||
|
/// </summary>
|
||||||
|
public void Write(TextWriter writer, FeatureContext ctx)
|
||||||
|
{
|
||||||
|
var currentPos = Vector.Zero;
|
||||||
|
var lastFeedVar = "";
|
||||||
|
var kerfEmitted = false;
|
||||||
|
|
||||||
|
// Find the pierce point from the first rapid move
|
||||||
|
var piercePoint = FindPiercePoint(ctx.Codes);
|
||||||
|
|
||||||
|
// 1. Rapid to pierce point (with line number if configured)
|
||||||
|
WriteRapidToPierce(writer, ctx.FeatureNumber, piercePoint);
|
||||||
|
|
||||||
|
// 2. Part name comment on first feature of each part
|
||||||
|
if (ctx.IsFirstFeatureOfPart && !string.IsNullOrEmpty(ctx.PartName))
|
||||||
|
writer.WriteLine(CoordinateFormatter.Comment($"PART: {ctx.PartName}"));
|
||||||
|
|
||||||
|
// 3. G89 process params (if RepeatG89BeforeEachFeature)
|
||||||
|
if (_config.RepeatG89BeforeEachFeature && _config.ProcessParameterMode == G89Mode.LibraryFile)
|
||||||
|
{
|
||||||
|
var lib = !string.IsNullOrEmpty(ctx.LibraryFile) ? ctx.LibraryFile : _config.DefaultLibraryFile;
|
||||||
|
var speedClass = _speedClassifier.Classify(ctx.CutDistance, ctx.SheetDiagonal);
|
||||||
|
var cutDist = _speedClassifier.FormatCutDist(ctx.CutDistance, ctx.SheetDiagonal);
|
||||||
|
writer.WriteLine($"G89 P {lib} ({speedClass} {cutDist})");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Pierce and start cut
|
||||||
|
writer.WriteLine("G84");
|
||||||
|
|
||||||
|
// 5. Anti-dive off
|
||||||
|
if (_config.UseAntiDive)
|
||||||
|
writer.WriteLine("M130 (ANTI DIVE OFF)");
|
||||||
|
|
||||||
|
// Update current position to pierce point
|
||||||
|
currentPos = piercePoint;
|
||||||
|
|
||||||
|
// 6. Lead-in + contour moves with kerf comp and feedrate variables
|
||||||
|
foreach (var code in ctx.Codes)
|
||||||
|
{
|
||||||
|
if (code is RapidMove)
|
||||||
|
continue; // skip rapids in contour (already handled above)
|
||||||
|
|
||||||
|
if (code is LinearMove linear)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
|
||||||
|
// Kerf compensation on first cutting move
|
||||||
|
if (!kerfEmitted && _config.KerfCompensation == KerfMode.ControllerSide)
|
||||||
|
{
|
||||||
|
sb.Append(_config.DefaultKerfSide == KerfSide.Left ? "G41" : "G42");
|
||||||
|
kerfEmitted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.Append($"G1X{_fmt.FormatCoord(linear.EndPoint.X)}Y{_fmt.FormatCoord(linear.EndPoint.Y)}");
|
||||||
|
|
||||||
|
// Feedrate
|
||||||
|
var feedVar = GetFeedVariable(linear.Layer);
|
||||||
|
if (feedVar != lastFeedVar)
|
||||||
|
{
|
||||||
|
sb.Append($"F{feedVar}");
|
||||||
|
lastFeedVar = feedVar;
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.WriteLine(sb.ToString());
|
||||||
|
currentPos = linear.EndPoint;
|
||||||
|
}
|
||||||
|
else if (code is ArcMove arc)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
|
||||||
|
// Kerf compensation on first cutting move
|
||||||
|
if (!kerfEmitted && _config.KerfCompensation == KerfMode.ControllerSide)
|
||||||
|
{
|
||||||
|
sb.Append(_config.DefaultKerfSide == KerfSide.Left ? "G41" : "G42");
|
||||||
|
kerfEmitted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// G2 = CW, G3 = CCW
|
||||||
|
var gCode = arc.Rotation == RotationType.CW ? "G2" : "G3";
|
||||||
|
sb.Append($"{gCode}X{_fmt.FormatCoord(arc.EndPoint.X)}Y{_fmt.FormatCoord(arc.EndPoint.Y)}");
|
||||||
|
|
||||||
|
// Convert absolute center to incremental I/J
|
||||||
|
var i = arc.CenterPoint.X - currentPos.X;
|
||||||
|
var j = arc.CenterPoint.Y - currentPos.Y;
|
||||||
|
sb.Append($"I{_fmt.FormatCoord(i)}J{_fmt.FormatCoord(j)}");
|
||||||
|
|
||||||
|
// Feedrate — full circles use multiplied feedrate
|
||||||
|
var isFullCircle = IsFullCircle(currentPos, arc.EndPoint);
|
||||||
|
var feedVar = isFullCircle ? "[#148*#128]" : GetFeedVariable(arc.Layer);
|
||||||
|
if (feedVar != lastFeedVar)
|
||||||
|
{
|
||||||
|
sb.Append($"F{feedVar}");
|
||||||
|
lastFeedVar = feedVar;
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.WriteLine(sb.ToString());
|
||||||
|
currentPos = arc.EndPoint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Cancel kerf compensation
|
||||||
|
if (kerfEmitted)
|
||||||
|
writer.WriteLine("G40");
|
||||||
|
|
||||||
|
// 8. Beam off
|
||||||
|
writer.WriteLine(_config.UseSpeedGas ? "M135" : "M35");
|
||||||
|
|
||||||
|
// 9. Anti-dive on
|
||||||
|
if (_config.UseAntiDive)
|
||||||
|
writer.WriteLine("M131 (ANTI DIVE ON)");
|
||||||
|
|
||||||
|
// 10. Head raise (unless last feature on sheet)
|
||||||
|
if (!ctx.IsLastFeatureOnSheet)
|
||||||
|
WriteM47(writer, ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Vector FindPiercePoint(List<ICode> codes)
|
||||||
|
{
|
||||||
|
foreach (var code in codes)
|
||||||
|
{
|
||||||
|
if (code is RapidMove rapid)
|
||||||
|
return rapid.EndPoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no rapid move, use the endpoint of the first motion
|
||||||
|
foreach (var code in codes)
|
||||||
|
{
|
||||||
|
if (code is Motion motion)
|
||||||
|
return motion.EndPoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Vector.Zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WriteRapidToPierce(TextWriter writer, int featureNumber, Vector piercePoint)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
|
||||||
|
if (_config.UseLineNumbers)
|
||||||
|
sb.Append($"N{featureNumber}");
|
||||||
|
|
||||||
|
sb.Append($"G0X{_fmt.FormatCoord(piercePoint.X)}Y{_fmt.FormatCoord(piercePoint.Y)}");
|
||||||
|
|
||||||
|
writer.WriteLine(sb.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WriteM47(TextWriter writer, FeatureContext ctx)
|
||||||
|
{
|
||||||
|
if (ctx.IsSafetyHeadraise && _config.SafetyHeadraiseDistance.HasValue)
|
||||||
|
{
|
||||||
|
writer.WriteLine($"M47 P{_config.SafetyHeadraiseDistance.Value}(Safety Headraise)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var mode = ctx.IsExteriorFeature ? _config.ExteriorM47 : _config.InteriorM47;
|
||||||
|
|
||||||
|
switch (mode)
|
||||||
|
{
|
||||||
|
case M47Mode.Always:
|
||||||
|
writer.WriteLine("M47");
|
||||||
|
break;
|
||||||
|
case M47Mode.BlockDelete:
|
||||||
|
writer.WriteLine("/M47");
|
||||||
|
break;
|
||||||
|
case M47Mode.None:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetFeedVariable(LayerType layer)
|
||||||
|
{
|
||||||
|
return layer switch
|
||||||
|
{
|
||||||
|
LayerType.Leadin => "#126",
|
||||||
|
LayerType.Cut => "#148",
|
||||||
|
_ => "#148"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsFullCircle(Vector start, Vector end)
|
||||||
|
{
|
||||||
|
return Tolerance.IsEqualTo(start.X, end.X) && Tolerance.IsEqualTo(start.Y, end.Y);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using OpenNest.CNC;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
|
namespace OpenNest.Posts.Cincinnati;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Writes a Cincinnati-format part sub-program definition.
|
||||||
|
/// Each sub-program contains the complete cutting sequence for one unique part geometry
|
||||||
|
/// (drawing + rotation), with coordinates normalized to origin (0,0).
|
||||||
|
/// Called via M98 from sheet sub-programs.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CincinnatiPartSubprogramWriter
|
||||||
|
{
|
||||||
|
private readonly CincinnatiPostConfig _config;
|
||||||
|
private readonly CincinnatiFeatureWriter _featureWriter;
|
||||||
|
|
||||||
|
public CincinnatiPartSubprogramWriter(CincinnatiPostConfig config)
|
||||||
|
{
|
||||||
|
_config = config;
|
||||||
|
_featureWriter = new CincinnatiFeatureWriter(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Writes a complete part sub-program for the given normalized program.
|
||||||
|
/// The program coordinates must already be normalized to origin (0,0).
|
||||||
|
/// </summary>
|
||||||
|
public void Write(TextWriter w, Program normalizedProgram, string drawingName,
|
||||||
|
int subNumber, string libraryFile, double sheetDiagonal)
|
||||||
|
{
|
||||||
|
var features = SplitFeatures(normalizedProgram.Codes);
|
||||||
|
if (features.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
w.WriteLine("(*****************************************************)");
|
||||||
|
w.WriteLine($":{subNumber}");
|
||||||
|
w.WriteLine(CoordinateFormatter.Comment($"PART: {drawingName}"));
|
||||||
|
|
||||||
|
for (var i = 0; i < features.Count; i++)
|
||||||
|
{
|
||||||
|
var codes = features[i];
|
||||||
|
var featureNumber = i == 0
|
||||||
|
? _config.FeatureLineNumberStart
|
||||||
|
: 1000 + i + 1;
|
||||||
|
var cutDistance = ComputeCutDistance(codes);
|
||||||
|
|
||||||
|
var ctx = new FeatureContext
|
||||||
|
{
|
||||||
|
Codes = codes,
|
||||||
|
FeatureNumber = featureNumber,
|
||||||
|
PartName = drawingName,
|
||||||
|
IsFirstFeatureOfPart = false,
|
||||||
|
IsLastFeatureOnSheet = i == features.Count - 1,
|
||||||
|
IsSafetyHeadraise = false,
|
||||||
|
IsExteriorFeature = false,
|
||||||
|
LibraryFile = libraryFile,
|
||||||
|
CutDistance = cutDistance,
|
||||||
|
SheetDiagonal = sheetDiagonal
|
||||||
|
};
|
||||||
|
|
||||||
|
_featureWriter.Write(w, ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteLine("G0X0Y0");
|
||||||
|
w.WriteLine($"M99(END OF {drawingName})");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a sub-program key for matching parts to their sub-programs.
|
||||||
|
/// </summary>
|
||||||
|
internal static (int drawingId, long rotationKey) SubprogramKey(Part part) =>
|
||||||
|
(part.BaseDrawing.Id, (long)System.Math.Round(part.Rotation * 1e6));
|
||||||
|
|
||||||
|
internal static List<List<ICode>> SplitFeatures(List<ICode> codes)
|
||||||
|
{
|
||||||
|
var features = new List<List<ICode>>();
|
||||||
|
List<ICode> current = null;
|
||||||
|
|
||||||
|
foreach (var code in codes)
|
||||||
|
{
|
||||||
|
if (code is RapidMove)
|
||||||
|
{
|
||||||
|
if (current != null)
|
||||||
|
features.Add(current);
|
||||||
|
current = new List<ICode> { code };
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
current ??= new List<ICode>();
|
||||||
|
current.Add(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current != null && current.Count > 0)
|
||||||
|
features.Add(current);
|
||||||
|
|
||||||
|
return features;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static double ComputeCutDistance(List<ICode> codes)
|
||||||
|
{
|
||||||
|
var distance = 0.0;
|
||||||
|
var currentPos = Vector.Zero;
|
||||||
|
|
||||||
|
foreach (var code in codes)
|
||||||
|
{
|
||||||
|
if (code is RapidMove rapid)
|
||||||
|
currentPos = rapid.EndPoint;
|
||||||
|
else if (code is LinearMove linear)
|
||||||
|
{
|
||||||
|
distance += currentPos.DistanceTo(linear.EndPoint);
|
||||||
|
currentPos = linear.EndPoint;
|
||||||
|
}
|
||||||
|
else if (code is ArcMove arc)
|
||||||
|
{
|
||||||
|
distance += currentPos.DistanceTo(arc.EndPoint);
|
||||||
|
currentPos = arc.EndPoint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return distance;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -88,7 +88,7 @@ namespace OpenNest.Posts.Cincinnati
|
|||||||
/// Configuration for Cincinnati post processor.
|
/// Configuration for Cincinnati post processor.
|
||||||
/// Defines machine-specific parameters, output format, and cutting strategies.
|
/// Defines machine-specific parameters, output format, and cutting strategies.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CincinnatiPostConfig
|
public sealed class CincinnatiPostConfig
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the configuration name/identifier.
|
/// Gets or sets the configuration name/identifier.
|
||||||
@@ -126,6 +126,20 @@ namespace OpenNest.Posts.Cincinnati
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int SheetSubprogramStart { get; set; } = 101;
|
public int SheetSubprogramStart { get; set; } = 101;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets whether to use M98 sub-programs for part geometry.
|
||||||
|
/// When enabled, each unique part geometry is written as a reusable sub-program
|
||||||
|
/// called via M98, reducing output size for nests with repeated parts.
|
||||||
|
/// Default: false
|
||||||
|
/// </summary>
|
||||||
|
public bool UsePartSubprograms { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the starting sub-program number for part geometry sub-programs.
|
||||||
|
/// Default: 200
|
||||||
|
/// </summary>
|
||||||
|
public int PartSubprogramStart { get; set; } = 200;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the subprogram number for variable declarations.
|
/// Gets or sets the subprogram number for variable declarations.
|
||||||
/// Default: 100
|
/// Default: 100
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using OpenNest.CNC;
|
||||||
|
|
||||||
|
namespace OpenNest.Posts.Cincinnati
|
||||||
|
{
|
||||||
|
public sealed class CincinnatiPostProcessor : IPostProcessor
|
||||||
|
{
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
|
{
|
||||||
|
WriteIndented = true,
|
||||||
|
Converters = { new JsonStringEnumConverter() }
|
||||||
|
};
|
||||||
|
|
||||||
|
public string Name => "Cincinnati CL-707";
|
||||||
|
public string Author => "OpenNest";
|
||||||
|
public string Description => "Cincinnati CL-707/CL-800/CL-900/CL-940/CLX family";
|
||||||
|
|
||||||
|
public CincinnatiPostConfig Config { get; }
|
||||||
|
|
||||||
|
public CincinnatiPostProcessor()
|
||||||
|
{
|
||||||
|
var configPath = GetConfigPath();
|
||||||
|
if (File.Exists(configPath))
|
||||||
|
{
|
||||||
|
var json = File.ReadAllText(configPath);
|
||||||
|
Config = JsonSerializer.Deserialize<CincinnatiPostConfig>(json, JsonOptions);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Config = new CincinnatiPostConfig();
|
||||||
|
SaveConfig();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public CincinnatiPostProcessor(CincinnatiPostConfig config)
|
||||||
|
{
|
||||||
|
Config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SaveConfig()
|
||||||
|
{
|
||||||
|
var configPath = GetConfigPath();
|
||||||
|
var json = JsonSerializer.Serialize(Config, JsonOptions);
|
||||||
|
File.WriteAllText(configPath, json);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetConfigPath()
|
||||||
|
{
|
||||||
|
var assemblyPath = typeof(CincinnatiPostProcessor).Assembly.Location;
|
||||||
|
var dir = Path.GetDirectoryName(assemblyPath);
|
||||||
|
var name = Path.GetFileNameWithoutExtension(assemblyPath);
|
||||||
|
return Path.Combine(dir, name + ".json");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Post(Nest nest, Stream outputStream)
|
||||||
|
{
|
||||||
|
// 1. Create variable manager and register standard variables
|
||||||
|
var vars = CreateVariableManager();
|
||||||
|
|
||||||
|
// 2. Filter to non-empty plates
|
||||||
|
var plates = nest.Plates
|
||||||
|
.Where(p => p.Parts.Count > 0)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// 3. Build part sub-program registry (if enabled)
|
||||||
|
Dictionary<(int, long), int> partSubprograms = null;
|
||||||
|
List<(int subNum, string name, Program program)> subprogramEntries = null;
|
||||||
|
|
||||||
|
if (Config.UsePartSubprograms)
|
||||||
|
{
|
||||||
|
partSubprograms = new Dictionary<(int, long), int>();
|
||||||
|
subprogramEntries = new List<(int, string, Program)>();
|
||||||
|
var nextSubNum = Config.PartSubprogramStart;
|
||||||
|
|
||||||
|
foreach (var plate in plates)
|
||||||
|
{
|
||||||
|
foreach (var part in plate.Parts)
|
||||||
|
{
|
||||||
|
if (part.BaseDrawing.IsCutOff) continue;
|
||||||
|
var key = CincinnatiPartSubprogramWriter.SubprogramKey(part);
|
||||||
|
if (!partSubprograms.ContainsKey(key))
|
||||||
|
{
|
||||||
|
var subNum = nextSubNum++;
|
||||||
|
partSubprograms[key] = subNum;
|
||||||
|
|
||||||
|
// Create normalized program at origin
|
||||||
|
var pgm = part.Program.Clone() as Program;
|
||||||
|
var bbox = pgm.BoundingBox();
|
||||||
|
pgm.Offset(-bbox.Location.X, -bbox.Location.Y);
|
||||||
|
|
||||||
|
subprogramEntries.Add((subNum, part.BaseDrawing.Name, pgm));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Create writers
|
||||||
|
var preamble = new CincinnatiPreambleWriter(Config);
|
||||||
|
var sheetWriter = new CincinnatiSheetWriter(Config, vars);
|
||||||
|
|
||||||
|
// 5. Build material description from first plate
|
||||||
|
var material = plates.FirstOrDefault()?.Material;
|
||||||
|
var materialDesc = material != null
|
||||||
|
? $"{material.Name}{(string.IsNullOrEmpty(material.Grade) ? "" : $", {material.Grade}")}"
|
||||||
|
: "";
|
||||||
|
|
||||||
|
// 6. Write to stream
|
||||||
|
using var writer = new StreamWriter(outputStream, Encoding.UTF8, 1024, leaveOpen: true);
|
||||||
|
|
||||||
|
// Main program
|
||||||
|
preamble.WriteMainProgram(writer, nest.Name ?? "NEST", materialDesc, plates.Count);
|
||||||
|
|
||||||
|
// Variable declaration subprogram
|
||||||
|
preamble.WriteVariableDeclaration(writer, vars);
|
||||||
|
|
||||||
|
// Sheet subprograms
|
||||||
|
for (var i = 0; i < plates.Count; i++)
|
||||||
|
{
|
||||||
|
var sheetIndex = i + 1;
|
||||||
|
var subNumber = Config.SheetSubprogramStart + i;
|
||||||
|
sheetWriter.Write(writer, plates[i], nest.Name ?? "NEST", sheetIndex, subNumber,
|
||||||
|
partSubprograms);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Part sub-programs (if enabled)
|
||||||
|
if (subprogramEntries != null)
|
||||||
|
{
|
||||||
|
var partSubWriter = new CincinnatiPartSubprogramWriter(Config);
|
||||||
|
var firstPlate = plates.FirstOrDefault();
|
||||||
|
var sheetDiagonal = firstPlate != null
|
||||||
|
? System.Math.Sqrt(firstPlate.Size.Width * firstPlate.Size.Width
|
||||||
|
+ firstPlate.Size.Length * firstPlate.Size.Length)
|
||||||
|
: 100.0;
|
||||||
|
|
||||||
|
foreach (var (subNum, name, pgm) in subprogramEntries)
|
||||||
|
{
|
||||||
|
partSubWriter.Write(writer, pgm, name, subNum,
|
||||||
|
Config.DefaultLibraryFile ?? "", sheetDiagonal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.Flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Post(Nest nest, string outputFile)
|
||||||
|
{
|
||||||
|
using var fs = new FileStream(outputFile, FileMode.Create, FileAccess.Write);
|
||||||
|
Post(nest, fs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ProgramVariableManager CreateVariableManager()
|
||||||
|
{
|
||||||
|
var vars = new ProgramVariableManager();
|
||||||
|
vars.GetOrCreate("ProcessFeedrate", 148); // Set by G89, no expression
|
||||||
|
vars.GetOrCreate("LeadInFeedrate", 126, $"[#148*{Config.LeadInFeedratePercent}]");
|
||||||
|
vars.GetOrCreate("LeadInArcLine2Feedrate", 127, $"[#148*{Config.LeadInArcLine2FeedratePercent}]");
|
||||||
|
vars.GetOrCreate("CircleFeedrate", 128, Config.CircleFeedrateMultiplier.ToString("0.#"));
|
||||||
|
return vars;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using OpenNest;
|
||||||
|
using OpenNest.CNC;
|
||||||
|
|
||||||
|
namespace OpenNest.Posts.Cincinnati;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Emits the main program header and variable declaration subprogram
|
||||||
|
/// for a Cincinnati laser post-processor output file.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CincinnatiPreambleWriter
|
||||||
|
{
|
||||||
|
private readonly CincinnatiPostConfig _config;
|
||||||
|
|
||||||
|
public CincinnatiPreambleWriter(CincinnatiPostConfig config)
|
||||||
|
{
|
||||||
|
_config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Writes the main program header block.
|
||||||
|
/// </summary>
|
||||||
|
public void WriteMainProgram(TextWriter w, string nestName, string materialDescription, int sheetCount)
|
||||||
|
{
|
||||||
|
w.WriteLine(CoordinateFormatter.Comment($"NEST {nestName}"));
|
||||||
|
w.WriteLine(CoordinateFormatter.Comment($"CONFIGURATION - {_config.ConfigurationName}"));
|
||||||
|
w.WriteLine(CoordinateFormatter.Comment(DateTime.Now.ToString("MM-dd-yyyy hh:mm:ss tt", System.Globalization.CultureInfo.InvariantCulture)));
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(materialDescription))
|
||||||
|
w.WriteLine(CoordinateFormatter.Comment($"Material = {materialDescription}"));
|
||||||
|
|
||||||
|
if (_config.UseExactStopMode)
|
||||||
|
w.WriteLine("G61");
|
||||||
|
|
||||||
|
w.WriteLine(CoordinateFormatter.Comment("MAIN PROGRAM"));
|
||||||
|
|
||||||
|
w.WriteLine(_config.PostedUnits == Units.Millimeters ? "G21" : "G20");
|
||||||
|
|
||||||
|
w.WriteLine("M42");
|
||||||
|
|
||||||
|
if (_config.ProcessParameterMode == G89Mode.LibraryFile && !string.IsNullOrEmpty(_config.DefaultLibraryFile))
|
||||||
|
w.WriteLine($"G89 P {_config.DefaultLibraryFile}");
|
||||||
|
|
||||||
|
w.WriteLine($"M98 P{_config.VariableDeclarationSubprogram} (Variable Declaration)");
|
||||||
|
|
||||||
|
w.WriteLine("GOTO1 (GOTO SHEET NUMBER)");
|
||||||
|
|
||||||
|
for (var i = 1; i <= sheetCount; i++)
|
||||||
|
{
|
||||||
|
var subNum = _config.SheetSubprogramStart + (i - 1);
|
||||||
|
w.WriteLine($"N{i}M98 P{subNum} (SHEET {i})");
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteLine("M42");
|
||||||
|
w.WriteLine("M30 (END OF MAIN)");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Writes the variable declaration subprogram block.
|
||||||
|
/// </summary>
|
||||||
|
public void WriteVariableDeclaration(TextWriter w, ProgramVariableManager vars)
|
||||||
|
{
|
||||||
|
w.WriteLine("(*****************************************************)");
|
||||||
|
w.WriteLine($":{_config.VariableDeclarationSubprogram}");
|
||||||
|
w.WriteLine("(Variable Declaration Start)");
|
||||||
|
|
||||||
|
foreach (var line in vars.EmitDeclarations())
|
||||||
|
w.WriteLine(line);
|
||||||
|
|
||||||
|
w.WriteLine("M99 (Variable Declaration End)");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,309 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using OpenNest.CNC;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
|
namespace OpenNest.Posts.Cincinnati;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Emits one Cincinnati-format sheet subprogram per plate.
|
||||||
|
/// Supports two modes: inline features (default) or M98 sub-program calls per part.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CincinnatiSheetWriter
|
||||||
|
{
|
||||||
|
private readonly CincinnatiPostConfig _config;
|
||||||
|
private readonly ProgramVariableManager _vars;
|
||||||
|
private readonly CoordinateFormatter _fmt;
|
||||||
|
private readonly CincinnatiFeatureWriter _featureWriter;
|
||||||
|
|
||||||
|
public CincinnatiSheetWriter(CincinnatiPostConfig config, ProgramVariableManager vars)
|
||||||
|
{
|
||||||
|
_config = config;
|
||||||
|
_vars = vars;
|
||||||
|
_fmt = new CoordinateFormatter(config.PostedAccuracy);
|
||||||
|
_featureWriter = new CincinnatiFeatureWriter(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Writes a complete sheet subprogram for the given plate.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="partSubprograms">
|
||||||
|
/// Optional mapping of (drawingId, rotationKey) to sub-program number.
|
||||||
|
/// When provided, non-cutoff parts are emitted as M98 calls instead of inline features.
|
||||||
|
/// </param>
|
||||||
|
public void Write(TextWriter w, Plate plate, string nestName, int sheetIndex, int subNumber,
|
||||||
|
Dictionary<(int, long), int> partSubprograms = null)
|
||||||
|
{
|
||||||
|
if (plate.Parts.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var width = plate.Size.Width;
|
||||||
|
var length = plate.Size.Length;
|
||||||
|
var sheetDiagonal = System.Math.Sqrt(width * width + length * length);
|
||||||
|
var libraryFile = _config.DefaultLibraryFile ?? "";
|
||||||
|
var varDeclSub = _config.VariableDeclarationSubprogram;
|
||||||
|
var partCount = plate.Parts.Count(p => !p.BaseDrawing.IsCutOff);
|
||||||
|
|
||||||
|
// 1. Sheet header
|
||||||
|
w.WriteLine("(*****************************************************)");
|
||||||
|
w.WriteLine($"( START OF {nestName}.{sheetIndex:D3} )");
|
||||||
|
w.WriteLine($":{subNumber}");
|
||||||
|
w.WriteLine($"( Sheet {sheetIndex} )");
|
||||||
|
w.WriteLine($"( Layout {sheetIndex} )");
|
||||||
|
w.WriteLine($"( SHEET NAME = {_fmt.FormatCoord(length)} X {_fmt.FormatCoord(width)} )");
|
||||||
|
w.WriteLine($"( Total parts on sheet = {partCount} )");
|
||||||
|
w.WriteLine($"#{_config.SheetWidthVariable}={_fmt.FormatCoord(width)}(SHEET WIDTH FOR CUTOFFS)");
|
||||||
|
w.WriteLine($"#{_config.SheetLengthVariable}={_fmt.FormatCoord(length)}(SHEET LENGTH FOR CUTOFFS)");
|
||||||
|
|
||||||
|
// 2. Coordinate setup
|
||||||
|
w.WriteLine("M42");
|
||||||
|
w.WriteLine("N10000");
|
||||||
|
w.WriteLine("G92X#5021Y#5022");
|
||||||
|
if (!string.IsNullOrEmpty(libraryFile))
|
||||||
|
w.WriteLine($"G89 P {libraryFile}");
|
||||||
|
w.WriteLine($"M98 P{varDeclSub} (Variable Declaration)");
|
||||||
|
w.WriteLine("G90");
|
||||||
|
w.WriteLine("M47(CPT)");
|
||||||
|
if (!string.IsNullOrEmpty(libraryFile))
|
||||||
|
w.WriteLine($"G89 P {libraryFile}");
|
||||||
|
w.WriteLine("GOTO1( Goto Feature )");
|
||||||
|
|
||||||
|
// 3. Order parts: non-cutoff sorted by Bottom then Left, cutoffs last
|
||||||
|
var nonCutoffParts = plate.Parts
|
||||||
|
.Where(p => !p.BaseDrawing.IsCutOff)
|
||||||
|
.OrderBy(p => p.Bottom)
|
||||||
|
.ThenBy(p => p.Left)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var cutoffParts = plate.Parts
|
||||||
|
.Where(p => p.BaseDrawing.IsCutOff)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var allParts = nonCutoffParts.Concat(cutoffParts).ToList();
|
||||||
|
|
||||||
|
// 4. Emit parts
|
||||||
|
if (partSubprograms != null)
|
||||||
|
WritePartsWithSubprograms(w, allParts, libraryFile, sheetDiagonal, partSubprograms);
|
||||||
|
else
|
||||||
|
WritePartsInline(w, allParts, libraryFile, sheetDiagonal);
|
||||||
|
|
||||||
|
// 5. Footer
|
||||||
|
w.WriteLine("M42");
|
||||||
|
w.WriteLine("G0X0Y0");
|
||||||
|
if (_config.PalletExchange != PalletMode.None)
|
||||||
|
w.WriteLine($"N{sheetIndex + 1}M50");
|
||||||
|
w.WriteLine($"M99(END OF {nestName}.{sheetIndex:D3})");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WritePartsWithSubprograms(TextWriter w, List<Part> allParts,
|
||||||
|
string libraryFile, double sheetDiagonal,
|
||||||
|
Dictionary<(int, long), int> partSubprograms)
|
||||||
|
{
|
||||||
|
var lastPartName = "";
|
||||||
|
var featureIndex = 0;
|
||||||
|
|
||||||
|
for (var p = 0; p < allParts.Count; p++)
|
||||||
|
{
|
||||||
|
var part = allParts[p];
|
||||||
|
var partName = part.BaseDrawing.Name;
|
||||||
|
var isNewPart = partName != lastPartName;
|
||||||
|
var isSafetyHeadraise = isNewPart && lastPartName != "";
|
||||||
|
var isLastPart = p == allParts.Count - 1;
|
||||||
|
|
||||||
|
var key = CincinnatiPartSubprogramWriter.SubprogramKey(part);
|
||||||
|
partSubprograms.TryGetValue(key, out var subNum);
|
||||||
|
var hasSubprogram = !part.BaseDrawing.IsCutOff && subNum != 0;
|
||||||
|
|
||||||
|
if (hasSubprogram)
|
||||||
|
{
|
||||||
|
WriteSubprogramCall(w, part, subNum, featureIndex, partName,
|
||||||
|
isSafetyHeadraise, isLastPart);
|
||||||
|
featureIndex++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Inline features for cutoffs or parts without sub-programs
|
||||||
|
var features = SplitPartFeatures(part);
|
||||||
|
for (var f = 0; f < features.Count; f++)
|
||||||
|
{
|
||||||
|
var featureNumber = featureIndex == 0
|
||||||
|
? _config.FeatureLineNumberStart
|
||||||
|
: 1000 + featureIndex + 1;
|
||||||
|
|
||||||
|
var isLastFeature = isLastPart && f == features.Count - 1;
|
||||||
|
var cutDistance = ComputeCutDistance(features[f]);
|
||||||
|
|
||||||
|
var ctx = new FeatureContext
|
||||||
|
{
|
||||||
|
Codes = features[f],
|
||||||
|
FeatureNumber = featureNumber,
|
||||||
|
PartName = partName,
|
||||||
|
IsFirstFeatureOfPart = isNewPart && f == 0,
|
||||||
|
IsLastFeatureOnSheet = isLastFeature,
|
||||||
|
IsSafetyHeadraise = isSafetyHeadraise && f == 0,
|
||||||
|
IsExteriorFeature = false,
|
||||||
|
LibraryFile = libraryFile,
|
||||||
|
CutDistance = cutDistance,
|
||||||
|
SheetDiagonal = sheetDiagonal
|
||||||
|
};
|
||||||
|
|
||||||
|
_featureWriter.Write(w, ctx);
|
||||||
|
featureIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lastPartName = partName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WriteSubprogramCall(TextWriter w, Part part, int subNum,
|
||||||
|
int featureIndex, string partName, bool isSafetyHeadraise, bool isLastPart)
|
||||||
|
{
|
||||||
|
// Safety headraise before rapid to new part
|
||||||
|
if (isSafetyHeadraise && _config.SafetyHeadraiseDistance.HasValue)
|
||||||
|
w.WriteLine($"M47 P{_config.SafetyHeadraiseDistance.Value}(Safety Headraise)");
|
||||||
|
|
||||||
|
// Rapid to part position (bounding box lower-left)
|
||||||
|
var featureNumber = featureIndex == 0
|
||||||
|
? _config.FeatureLineNumberStart
|
||||||
|
: 1000 + featureIndex + 1;
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
if (_config.UseLineNumbers)
|
||||||
|
sb.Append($"N{featureNumber}");
|
||||||
|
sb.Append($"G0X{_fmt.FormatCoord(part.Left)}Y{_fmt.FormatCoord(part.Bottom)}");
|
||||||
|
w.WriteLine(sb.ToString());
|
||||||
|
|
||||||
|
// Part name comment
|
||||||
|
w.WriteLine(CoordinateFormatter.Comment($"PART: {partName}"));
|
||||||
|
|
||||||
|
// Set local coordinate system at part position
|
||||||
|
w.WriteLine("G92X0Y0");
|
||||||
|
|
||||||
|
// Call part sub-program
|
||||||
|
w.WriteLine($"M98P{subNum}({partName})");
|
||||||
|
|
||||||
|
// Restore sheet coordinate system
|
||||||
|
w.WriteLine($"G92X{_fmt.FormatCoord(part.Left)}Y{_fmt.FormatCoord(part.Bottom)}");
|
||||||
|
|
||||||
|
// Head raise (unless last part on sheet)
|
||||||
|
if (!isLastPart)
|
||||||
|
w.WriteLine("M47");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WritePartsInline(TextWriter w, List<Part> allParts,
|
||||||
|
string libraryFile, double sheetDiagonal)
|
||||||
|
{
|
||||||
|
// Multi-contour splitting
|
||||||
|
var features = new List<(Part part, List<ICode> codes)>();
|
||||||
|
foreach (var part in allParts)
|
||||||
|
{
|
||||||
|
List<ICode> current = null;
|
||||||
|
foreach (var code in part.Program.Codes)
|
||||||
|
{
|
||||||
|
if (code is RapidMove)
|
||||||
|
{
|
||||||
|
if (current != null)
|
||||||
|
features.Add((part, current));
|
||||||
|
current = new List<ICode> { code };
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
current ??= new List<ICode>();
|
||||||
|
current.Add(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (current != null && current.Count > 0)
|
||||||
|
features.Add((part, current));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit features
|
||||||
|
var lastPartName = "";
|
||||||
|
for (var i = 0; i < features.Count; i++)
|
||||||
|
{
|
||||||
|
var (part, codes) = features[i];
|
||||||
|
var partName = part.BaseDrawing.Name;
|
||||||
|
var isFirstFeatureOfPart = partName != lastPartName;
|
||||||
|
var isSafetyHeadraise = partName != lastPartName && lastPartName != "";
|
||||||
|
var isLastFeature = i == features.Count - 1;
|
||||||
|
|
||||||
|
var featureNumber = i == 0
|
||||||
|
? _config.FeatureLineNumberStart
|
||||||
|
: 1000 + i + 1;
|
||||||
|
|
||||||
|
var cutDistance = ComputeCutDistance(codes);
|
||||||
|
|
||||||
|
var ctx = new FeatureContext
|
||||||
|
{
|
||||||
|
Codes = codes,
|
||||||
|
FeatureNumber = featureNumber,
|
||||||
|
PartName = partName,
|
||||||
|
IsFirstFeatureOfPart = isFirstFeatureOfPart,
|
||||||
|
IsLastFeatureOnSheet = isLastFeature,
|
||||||
|
IsSafetyHeadraise = isSafetyHeadraise,
|
||||||
|
IsExteriorFeature = false,
|
||||||
|
LibraryFile = libraryFile,
|
||||||
|
CutDistance = cutDistance,
|
||||||
|
SheetDiagonal = sheetDiagonal
|
||||||
|
};
|
||||||
|
|
||||||
|
_featureWriter.Write(w, ctx);
|
||||||
|
lastPartName = partName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<List<ICode>> SplitPartFeatures(Part part)
|
||||||
|
{
|
||||||
|
var features = new List<List<ICode>>();
|
||||||
|
List<ICode> current = null;
|
||||||
|
|
||||||
|
foreach (var code in part.Program.Codes)
|
||||||
|
{
|
||||||
|
if (code is RapidMove)
|
||||||
|
{
|
||||||
|
if (current != null)
|
||||||
|
features.Add(current);
|
||||||
|
current = new List<ICode> { code };
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
current ??= new List<ICode>();
|
||||||
|
current.Add(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current != null && current.Count > 0)
|
||||||
|
features.Add(current);
|
||||||
|
|
||||||
|
return features;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double ComputeCutDistance(List<ICode> codes)
|
||||||
|
{
|
||||||
|
var distance = 0.0;
|
||||||
|
var currentPos = Vector.Zero;
|
||||||
|
|
||||||
|
foreach (var code in codes)
|
||||||
|
{
|
||||||
|
if (code is RapidMove rapid)
|
||||||
|
{
|
||||||
|
currentPos = rapid.EndPoint;
|
||||||
|
}
|
||||||
|
else if (code is LinearMove linear)
|
||||||
|
{
|
||||||
|
distance += currentPos.DistanceTo(linear.EndPoint);
|
||||||
|
currentPos = linear.EndPoint;
|
||||||
|
}
|
||||||
|
else if (code is ArcMove arc)
|
||||||
|
{
|
||||||
|
distance += currentPos.DistanceTo(arc.EndPoint);
|
||||||
|
currentPos = arc.EndPoint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return distance;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,7 +13,8 @@ namespace OpenNest.Posts.Cincinnati
|
|||||||
|
|
||||||
public string FormatCoord(double value)
|
public string FormatCoord(double value)
|
||||||
{
|
{
|
||||||
return System.Math.Round(value, _accuracy).ToString(_format);
|
return System.Math.Round(value, _accuracy)
|
||||||
|
.ToString(_format, System.Globalization.CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string Comment(string text) => $"( {text} )";
|
public static string Comment(string text) => $"( {text} )";
|
||||||
|
|||||||
@@ -6,4 +6,11 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\OpenNest.Core\OpenNest.Core.csproj" />
|
<ProjectReference Include="..\OpenNest.Core\OpenNest.Core.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<Target Name="CopyToPostsDir" AfterTargets="Build">
|
||||||
|
<PropertyGroup>
|
||||||
|
<PostsDir>..\OpenNest\bin\$(Configuration)\$(TargetFramework)\Posts\</PostsDir>
|
||||||
|
</PropertyGroup>
|
||||||
|
<MakeDir Directories="$(PostsDir)" />
|
||||||
|
<Copy SourceFiles="$(TargetPath)" DestinationFolder="$(PostsDir)" SkipUnchangedFiles="true" />
|
||||||
|
</Target>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ namespace OpenNest.Posts.Cincinnati
|
|||||||
public string Classify(double contourLength, double sheetDiagonal)
|
public string Classify(double contourLength, double sheetDiagonal)
|
||||||
{
|
{
|
||||||
var ratio = contourLength / sheetDiagonal;
|
var ratio = contourLength / sheetDiagonal;
|
||||||
if (ratio > FastThreshold) return "FAST";
|
if (ratio >= FastThreshold) return "FAST";
|
||||||
if (ratio <= SlowThreshold) return "SLOW";
|
if (ratio <= SlowThreshold) return "SLOW";
|
||||||
return "MEDIUM";
|
return "MEDIUM";
|
||||||
}
|
}
|
||||||
@@ -22,7 +22,7 @@ namespace OpenNest.Posts.Cincinnati
|
|||||||
{
|
{
|
||||||
// Cincinnati convention: no leading zero for values < 1 (e.g., ".8702" not "0.8702")
|
// Cincinnati convention: no leading zero for values < 1 (e.g., ".8702" not "0.8702")
|
||||||
var rounded = System.Math.Round(value, 4);
|
var rounded = System.Math.Round(value, 4);
|
||||||
var str = rounded.ToString("0.####");
|
var str = rounded.ToString("0.####", System.Globalization.CultureInfo.InvariantCulture);
|
||||||
if (rounded > 0 && rounded < 1 && str.StartsWith("0."))
|
if (rounded > 0 && rounded < 1 && str.StartsWith("0."))
|
||||||
return str.Substring(1);
|
return str.Substring(1);
|
||||||
return str;
|
return str;
|
||||||
|
|||||||
@@ -0,0 +1,437 @@
|
|||||||
|
using OpenNest.CNC;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using OpenNest.Posts.Cincinnati;
|
||||||
|
|
||||||
|
namespace OpenNest.Tests.Cincinnati;
|
||||||
|
|
||||||
|
public class CincinnatiFeatureWriterTests
|
||||||
|
{
|
||||||
|
private static CincinnatiPostConfig DefaultConfig() => new()
|
||||||
|
{
|
||||||
|
UseLineNumbers = true,
|
||||||
|
FeatureLineNumberStart = 1,
|
||||||
|
UseAntiDive = true,
|
||||||
|
KerfCompensation = KerfMode.ControllerSide,
|
||||||
|
DefaultKerfSide = KerfSide.Left,
|
||||||
|
RepeatG89BeforeEachFeature = true,
|
||||||
|
ProcessParameterMode = G89Mode.LibraryFile,
|
||||||
|
DefaultLibraryFile = "MILD10",
|
||||||
|
InteriorM47 = M47Mode.Always,
|
||||||
|
ExteriorM47 = M47Mode.Always,
|
||||||
|
UseSpeedGas = false,
|
||||||
|
PostedAccuracy = 4,
|
||||||
|
SafetyHeadraiseDistance = 2000
|
||||||
|
};
|
||||||
|
|
||||||
|
private static FeatureContext SimpleContext(List<ICode>? codes = null) => new()
|
||||||
|
{
|
||||||
|
Codes = codes ?? new List<ICode>
|
||||||
|
{
|
||||||
|
new RapidMove(13.401, 57.4895),
|
||||||
|
new LinearMove(14.0, 57.5) { Layer = LayerType.Leadin },
|
||||||
|
new LinearMove(20.0, 57.5) { Layer = LayerType.Cut }
|
||||||
|
},
|
||||||
|
FeatureNumber = 1,
|
||||||
|
PartName = "BRACKET",
|
||||||
|
IsFirstFeatureOfPart = true,
|
||||||
|
IsLastFeatureOnSheet = false,
|
||||||
|
IsSafetyHeadraise = false,
|
||||||
|
IsExteriorFeature = false,
|
||||||
|
LibraryFile = "MILD10",
|
||||||
|
CutDistance = 18.0,
|
||||||
|
SheetDiagonal = 30.0
|
||||||
|
};
|
||||||
|
|
||||||
|
private static string WriteFeature(CincinnatiPostConfig config, FeatureContext ctx)
|
||||||
|
{
|
||||||
|
var writer = new CincinnatiFeatureWriter(config);
|
||||||
|
using var sw = new StringWriter();
|
||||||
|
writer.Write(sw, ctx);
|
||||||
|
return sw.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RapidToPiercePoint_WithLineNumber()
|
||||||
|
{
|
||||||
|
var config = DefaultConfig();
|
||||||
|
var ctx = SimpleContext();
|
||||||
|
var output = WriteFeature(config, ctx);
|
||||||
|
var lines = output.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
|
||||||
|
Assert.StartsWith("N1G0X13.401Y57.4895", lines[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RapidToPiercePoint_WithoutLineNumber()
|
||||||
|
{
|
||||||
|
var config = DefaultConfig();
|
||||||
|
config.UseLineNumbers = false;
|
||||||
|
var ctx = SimpleContext();
|
||||||
|
var output = WriteFeature(config, ctx);
|
||||||
|
var lines = output.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
|
||||||
|
Assert.StartsWith("G0X13.401Y57.4895", lines[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void G84_PierceEmitted()
|
||||||
|
{
|
||||||
|
var config = DefaultConfig();
|
||||||
|
var ctx = SimpleContext();
|
||||||
|
var output = WriteFeature(config, ctx);
|
||||||
|
|
||||||
|
Assert.Contains("G84", output);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AntiDive_M130M131_EmittedWhenEnabled()
|
||||||
|
{
|
||||||
|
var config = DefaultConfig();
|
||||||
|
config.UseAntiDive = true;
|
||||||
|
var ctx = SimpleContext();
|
||||||
|
var output = WriteFeature(config, ctx);
|
||||||
|
|
||||||
|
Assert.Contains("M130 (ANTI DIVE OFF)", output);
|
||||||
|
Assert.Contains("M131 (ANTI DIVE ON)", output);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AntiDive_NotEmittedWhenDisabled()
|
||||||
|
{
|
||||||
|
var config = DefaultConfig();
|
||||||
|
config.UseAntiDive = false;
|
||||||
|
var ctx = SimpleContext();
|
||||||
|
var output = WriteFeature(config, ctx);
|
||||||
|
|
||||||
|
Assert.DoesNotContain("M130", output);
|
||||||
|
Assert.DoesNotContain("M131", output);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void KerfCompensation_G41G40_EmittedWhenControllerSide()
|
||||||
|
{
|
||||||
|
var config = DefaultConfig();
|
||||||
|
config.KerfCompensation = KerfMode.ControllerSide;
|
||||||
|
config.DefaultKerfSide = KerfSide.Left;
|
||||||
|
var ctx = SimpleContext();
|
||||||
|
var output = WriteFeature(config, ctx);
|
||||||
|
|
||||||
|
Assert.Contains("G41", output);
|
||||||
|
Assert.Contains("G40", output);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void KerfCompensation_G42_EmittedForRightSide()
|
||||||
|
{
|
||||||
|
var config = DefaultConfig();
|
||||||
|
config.KerfCompensation = KerfMode.ControllerSide;
|
||||||
|
config.DefaultKerfSide = KerfSide.Right;
|
||||||
|
var ctx = SimpleContext();
|
||||||
|
var output = WriteFeature(config, ctx);
|
||||||
|
|
||||||
|
Assert.Contains("G42", output);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void KerfCompensation_NotEmittedWhenPreApplied()
|
||||||
|
{
|
||||||
|
var config = DefaultConfig();
|
||||||
|
config.KerfCompensation = KerfMode.PreApplied;
|
||||||
|
var ctx = SimpleContext();
|
||||||
|
var output = WriteFeature(config, ctx);
|
||||||
|
|
||||||
|
Assert.DoesNotContain("G41", output);
|
||||||
|
Assert.DoesNotContain("G42", output);
|
||||||
|
Assert.DoesNotContain("G40", output);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void M35_BeamOffEmitted()
|
||||||
|
{
|
||||||
|
var config = DefaultConfig();
|
||||||
|
config.UseSpeedGas = false;
|
||||||
|
var ctx = SimpleContext();
|
||||||
|
var output = WriteFeature(config, ctx);
|
||||||
|
|
||||||
|
Assert.Contains("M35", output);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void M135_BeamOffEmittedWhenSpeedGas()
|
||||||
|
{
|
||||||
|
var config = DefaultConfig();
|
||||||
|
config.UseSpeedGas = true;
|
||||||
|
var ctx = SimpleContext();
|
||||||
|
var output = WriteFeature(config, ctx);
|
||||||
|
|
||||||
|
Assert.Contains("M135", output);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void M47_EmittedWhenNotLastFeature()
|
||||||
|
{
|
||||||
|
var config = DefaultConfig();
|
||||||
|
var ctx = SimpleContext();
|
||||||
|
ctx.IsLastFeatureOnSheet = false;
|
||||||
|
var output = WriteFeature(config, ctx);
|
||||||
|
|
||||||
|
Assert.Contains("M47", output);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void M47_OmittedWhenLastFeatureOnSheet()
|
||||||
|
{
|
||||||
|
var config = DefaultConfig();
|
||||||
|
var ctx = SimpleContext();
|
||||||
|
ctx.IsLastFeatureOnSheet = true;
|
||||||
|
var output = WriteFeature(config, ctx);
|
||||||
|
|
||||||
|
// M47 should not appear, but M35 should still be there
|
||||||
|
var lines = output.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
Assert.DoesNotContain(lines, l => l.Trim() == "M47" || l.Trim() == "/M47");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void M47_BlockDeleteMode_EmitsSlashM47()
|
||||||
|
{
|
||||||
|
var config = DefaultConfig();
|
||||||
|
config.InteriorM47 = M47Mode.BlockDelete;
|
||||||
|
var ctx = SimpleContext();
|
||||||
|
ctx.IsExteriorFeature = false;
|
||||||
|
ctx.IsLastFeatureOnSheet = false;
|
||||||
|
var output = WriteFeature(config, ctx);
|
||||||
|
|
||||||
|
Assert.Contains("/M47", output);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void M47_NoneMode_NoM47Emitted()
|
||||||
|
{
|
||||||
|
var config = DefaultConfig();
|
||||||
|
config.InteriorM47 = M47Mode.None;
|
||||||
|
var ctx = SimpleContext();
|
||||||
|
ctx.IsExteriorFeature = false;
|
||||||
|
ctx.IsLastFeatureOnSheet = false;
|
||||||
|
var output = WriteFeature(config, ctx);
|
||||||
|
|
||||||
|
var lines = output.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
Assert.DoesNotContain(lines, l => l.Trim() == "M47" || l.Trim() == "/M47");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ArcIJ_ConvertedFromAbsoluteToIncremental()
|
||||||
|
{
|
||||||
|
var config = DefaultConfig();
|
||||||
|
// Arc starts at rapid endpoint (10, 20), center at (15, 20) absolute
|
||||||
|
// So incremental I = 15 - 10 = 5, J = 20 - 20 = 0
|
||||||
|
var codes = new List<ICode>
|
||||||
|
{
|
||||||
|
new RapidMove(10.0, 20.0),
|
||||||
|
new ArcMove(
|
||||||
|
endPoint: new Vector(10.0, 20.0),
|
||||||
|
centerPoint: new Vector(15.0, 20.0),
|
||||||
|
rotation: RotationType.CW
|
||||||
|
) { Layer = LayerType.Cut }
|
||||||
|
};
|
||||||
|
|
||||||
|
var ctx = SimpleContext(codes);
|
||||||
|
var output = WriteFeature(config, ctx);
|
||||||
|
|
||||||
|
// Should contain incremental I=5, J=0
|
||||||
|
Assert.Contains("I5", output);
|
||||||
|
Assert.Contains("J0", output);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ArcMove_G2ForCW_G3ForCCW()
|
||||||
|
{
|
||||||
|
var config = DefaultConfig();
|
||||||
|
var cwCodes = new List<ICode>
|
||||||
|
{
|
||||||
|
new RapidMove(10.0, 20.0),
|
||||||
|
new ArcMove(new Vector(20.0, 20.0), new Vector(15.0, 20.0), RotationType.CW) { Layer = LayerType.Cut }
|
||||||
|
};
|
||||||
|
var ccwCodes = new List<ICode>
|
||||||
|
{
|
||||||
|
new RapidMove(10.0, 20.0),
|
||||||
|
new ArcMove(new Vector(20.0, 20.0), new Vector(15.0, 20.0), RotationType.CCW) { Layer = LayerType.Cut }
|
||||||
|
};
|
||||||
|
|
||||||
|
var cwOutput = WriteFeature(config, SimpleContext(cwCodes));
|
||||||
|
var ccwOutput = WriteFeature(config, SimpleContext(ccwCodes));
|
||||||
|
|
||||||
|
Assert.Contains("G2X", cwOutput);
|
||||||
|
Assert.Contains("G3X", ccwOutput);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PartNameComment_EmittedOnFirstFeature()
|
||||||
|
{
|
||||||
|
var config = DefaultConfig();
|
||||||
|
var ctx = SimpleContext();
|
||||||
|
ctx.IsFirstFeatureOfPart = true;
|
||||||
|
ctx.PartName = "FLANGE";
|
||||||
|
var output = WriteFeature(config, ctx);
|
||||||
|
|
||||||
|
Assert.Contains("( PART: FLANGE )", output);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PartNameComment_NotEmittedOnSubsequentFeatures()
|
||||||
|
{
|
||||||
|
var config = DefaultConfig();
|
||||||
|
var ctx = SimpleContext();
|
||||||
|
ctx.IsFirstFeatureOfPart = false;
|
||||||
|
ctx.PartName = "FLANGE";
|
||||||
|
var output = WriteFeature(config, ctx);
|
||||||
|
|
||||||
|
Assert.DoesNotContain("PART:", output);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void G89_EmittedWhenRepeatEnabled()
|
||||||
|
{
|
||||||
|
var config = DefaultConfig();
|
||||||
|
config.RepeatG89BeforeEachFeature = true;
|
||||||
|
config.ProcessParameterMode = G89Mode.LibraryFile;
|
||||||
|
config.DefaultLibraryFile = "MILD10";
|
||||||
|
var ctx = SimpleContext();
|
||||||
|
ctx.LibraryFile = "MILD10";
|
||||||
|
ctx.CutDistance = 18.0;
|
||||||
|
ctx.SheetDiagonal = 30.0;
|
||||||
|
var output = WriteFeature(config, ctx);
|
||||||
|
|
||||||
|
Assert.Contains("G89 P MILD10", output);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void G89_NotEmittedWhenRepeatDisabled()
|
||||||
|
{
|
||||||
|
var config = DefaultConfig();
|
||||||
|
config.RepeatG89BeforeEachFeature = false;
|
||||||
|
var ctx = SimpleContext();
|
||||||
|
var output = WriteFeature(config, ctx);
|
||||||
|
|
||||||
|
Assert.DoesNotContain("G89", output);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FeedrateModalSuppression_OnlyEmitsOnChange()
|
||||||
|
{
|
||||||
|
var config = DefaultConfig();
|
||||||
|
config.KerfCompensation = KerfMode.PreApplied; // simplify output
|
||||||
|
var codes = new List<ICode>
|
||||||
|
{
|
||||||
|
new RapidMove(1.0, 1.0),
|
||||||
|
new LinearMove(2.0, 1.0) { Layer = LayerType.Cut },
|
||||||
|
new LinearMove(3.0, 1.0) { Layer = LayerType.Cut },
|
||||||
|
new LinearMove(4.0, 1.0) { Layer = LayerType.Cut }
|
||||||
|
};
|
||||||
|
var ctx = SimpleContext(codes);
|
||||||
|
var output = WriteFeature(config, ctx);
|
||||||
|
|
||||||
|
// F#148 should appear only once (on the first cut move)
|
||||||
|
var count = CountOccurrences(output, "F#148");
|
||||||
|
Assert.Equal(1, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LeadinFeedrate_UsesVariable126()
|
||||||
|
{
|
||||||
|
var config = DefaultConfig();
|
||||||
|
config.KerfCompensation = KerfMode.PreApplied; // simplify output
|
||||||
|
var codes = new List<ICode>
|
||||||
|
{
|
||||||
|
new RapidMove(1.0, 1.0),
|
||||||
|
new LinearMove(2.0, 1.0) { Layer = LayerType.Leadin }
|
||||||
|
};
|
||||||
|
var ctx = SimpleContext(codes);
|
||||||
|
var output = WriteFeature(config, ctx);
|
||||||
|
|
||||||
|
Assert.Contains("F#126", output);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FullCircleArc_UsesMultipliedFeedrate()
|
||||||
|
{
|
||||||
|
var config = DefaultConfig();
|
||||||
|
config.KerfCompensation = KerfMode.PreApplied;
|
||||||
|
// Full circle: start == end
|
||||||
|
var codes = new List<ICode>
|
||||||
|
{
|
||||||
|
new RapidMove(10.0, 20.0),
|
||||||
|
new ArcMove(new Vector(10.0, 20.0), new Vector(15.0, 20.0), RotationType.CW) { Layer = LayerType.Cut }
|
||||||
|
};
|
||||||
|
var ctx = SimpleContext(codes);
|
||||||
|
var output = WriteFeature(config, ctx);
|
||||||
|
|
||||||
|
Assert.Contains("F[#148*#128]", output);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SafetyHeadraise_EmitsM47WithDistance()
|
||||||
|
{
|
||||||
|
var config = DefaultConfig();
|
||||||
|
config.SafetyHeadraiseDistance = 2000;
|
||||||
|
var ctx = SimpleContext();
|
||||||
|
ctx.IsSafetyHeadraise = true;
|
||||||
|
ctx.IsLastFeatureOnSheet = false;
|
||||||
|
var output = WriteFeature(config, ctx);
|
||||||
|
|
||||||
|
Assert.Contains("M47 P2000(Safety Headraise)", output);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ExteriorM47Mode_UsesExteriorConfig()
|
||||||
|
{
|
||||||
|
var config = DefaultConfig();
|
||||||
|
config.ExteriorM47 = M47Mode.BlockDelete;
|
||||||
|
config.InteriorM47 = M47Mode.Always;
|
||||||
|
var ctx = SimpleContext();
|
||||||
|
ctx.IsExteriorFeature = true;
|
||||||
|
ctx.IsLastFeatureOnSheet = false;
|
||||||
|
var output = WriteFeature(config, ctx);
|
||||||
|
|
||||||
|
Assert.Contains("/M47", output);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OutputSequence_CorrectOrder()
|
||||||
|
{
|
||||||
|
var config = DefaultConfig();
|
||||||
|
var ctx = SimpleContext();
|
||||||
|
var output = WriteFeature(config, ctx);
|
||||||
|
var lines = output.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
|
||||||
|
// Find indices of key lines
|
||||||
|
var rapidIdx = Array.FindIndex(lines, l => l.Contains("G0X"));
|
||||||
|
var partIdx = Array.FindIndex(lines, l => l.Contains("PART:"));
|
||||||
|
var g89Idx = Array.FindIndex(lines, l => l.Contains("G89"));
|
||||||
|
var g84Idx = Array.FindIndex(lines, l => l.Contains("G84"));
|
||||||
|
var m130Idx = Array.FindIndex(lines, l => l.Contains("M130"));
|
||||||
|
var g40Idx = Array.FindIndex(lines, l => l.Contains("G40"));
|
||||||
|
var m35Idx = Array.FindIndex(lines, l => l.Contains("M35"));
|
||||||
|
var m131Idx = Array.FindIndex(lines, l => l.Contains("M131"));
|
||||||
|
var m47Idx = Array.FindIndex(lines, l => l.Trim() == "M47");
|
||||||
|
|
||||||
|
Assert.True(rapidIdx < partIdx, "Rapid should come before part comment");
|
||||||
|
Assert.True(partIdx < g89Idx, "Part comment should come before G89");
|
||||||
|
Assert.True(g89Idx < g84Idx, "G89 should come before G84");
|
||||||
|
Assert.True(g84Idx < m130Idx, "G84 should come before M130");
|
||||||
|
Assert.True(g40Idx < m35Idx, "G40 should come before M35");
|
||||||
|
Assert.True(m35Idx < m131Idx, "M35 should come before M131");
|
||||||
|
Assert.True(m131Idx < m47Idx, "M131 should come before M47");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int CountOccurrences(string text, string pattern)
|
||||||
|
{
|
||||||
|
var count = 0;
|
||||||
|
var idx = 0;
|
||||||
|
while ((idx = text.IndexOf(pattern, idx, StringComparison.Ordinal)) != -1)
|
||||||
|
{
|
||||||
|
count++;
|
||||||
|
idx += pattern.Length;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,322 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using OpenNest.CNC;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using OpenNest.Posts.Cincinnati;
|
||||||
|
|
||||||
|
namespace OpenNest.Tests.Cincinnati;
|
||||||
|
|
||||||
|
public class CincinnatiPostProcessorTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Post_ProducesOutput_ForSinglePlateNest()
|
||||||
|
{
|
||||||
|
var nest = CreateTestNest();
|
||||||
|
var config = new CincinnatiPostConfig
|
||||||
|
{
|
||||||
|
ConfigurationName = "CL940",
|
||||||
|
DefaultLibraryFile = "MS135N2PANEL.lib",
|
||||||
|
PostedAccuracy = 4
|
||||||
|
};
|
||||||
|
var post = new CincinnatiPostProcessor(config);
|
||||||
|
|
||||||
|
using var ms = new MemoryStream();
|
||||||
|
post.Post(nest, ms);
|
||||||
|
|
||||||
|
var output = Encoding.UTF8.GetString(ms.ToArray());
|
||||||
|
|
||||||
|
// Main program elements
|
||||||
|
Assert.Contains("( NEST TestNest )", output);
|
||||||
|
Assert.Contains("( CONFIGURATION - CL940 )", output);
|
||||||
|
Assert.Contains("G20", output);
|
||||||
|
Assert.Contains("M30 (END OF MAIN)", output);
|
||||||
|
|
||||||
|
// Variable declaration
|
||||||
|
Assert.Contains(":100", output);
|
||||||
|
Assert.Contains("#126=", output);
|
||||||
|
|
||||||
|
// Sheet subprogram
|
||||||
|
Assert.Contains(":101", output);
|
||||||
|
Assert.Contains("( Sheet 1 )", output);
|
||||||
|
Assert.Contains("G84", output);
|
||||||
|
Assert.Contains("M99", output);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Post_ImplementsIPostProcessor()
|
||||||
|
{
|
||||||
|
var post = new CincinnatiPostProcessor(new CincinnatiPostConfig());
|
||||||
|
IPostProcessor pp = post;
|
||||||
|
|
||||||
|
Assert.Equal("Cincinnati CL-707", pp.Name);
|
||||||
|
Assert.Equal("OpenNest", pp.Author);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Post_SkipsEmptyPlates()
|
||||||
|
{
|
||||||
|
var nest = new Nest("TestNest");
|
||||||
|
nest.Plates.Add(new Plate(48, 96)); // empty plate
|
||||||
|
var plate2 = new Plate(48, 96);
|
||||||
|
plate2.Parts.Add(new Part(new Drawing("Part1", CreateSquareProgram())));
|
||||||
|
nest.Plates.Add(plate2);
|
||||||
|
|
||||||
|
var config = new CincinnatiPostConfig { PostedAccuracy = 4 };
|
||||||
|
var post = new CincinnatiPostProcessor(config);
|
||||||
|
|
||||||
|
using var ms = new MemoryStream();
|
||||||
|
post.Post(nest, ms);
|
||||||
|
|
||||||
|
var output = Encoding.UTF8.GetString(ms.ToArray());
|
||||||
|
|
||||||
|
// Should only have one sheet subprogram call in main
|
||||||
|
Assert.Contains("N1M98 P101 (SHEET 1)", output);
|
||||||
|
Assert.DoesNotContain("SHEET 2", output);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Post_ToFile_CreatesFile()
|
||||||
|
{
|
||||||
|
var nest = CreateTestNest();
|
||||||
|
var config = new CincinnatiPostConfig { PostedAccuracy = 4 };
|
||||||
|
var post = new CincinnatiPostProcessor(config);
|
||||||
|
var tempFile = Path.GetTempFileName() + ".CNC";
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
post.Post(nest, tempFile);
|
||||||
|
Assert.True(File.Exists(tempFile));
|
||||||
|
var content = File.ReadAllText(tempFile);
|
||||||
|
Assert.Contains("M30", content);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (File.Exists(tempFile))
|
||||||
|
File.Delete(tempFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Config_RoundTripsAsJson()
|
||||||
|
{
|
||||||
|
var config = new CincinnatiPostConfig
|
||||||
|
{
|
||||||
|
ConfigurationName = "CL940_CORONA",
|
||||||
|
DefaultLibraryFile = "MS135N2PANEL.lib",
|
||||||
|
PostedUnits = Units.Inches,
|
||||||
|
KerfCompensation = KerfMode.ControllerSide,
|
||||||
|
UseAntiDive = true
|
||||||
|
};
|
||||||
|
|
||||||
|
var opts = new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
WriteIndented = true,
|
||||||
|
Converters = { new JsonStringEnumConverter() }
|
||||||
|
};
|
||||||
|
var json = JsonSerializer.Serialize(config, opts);
|
||||||
|
var deserialized = JsonSerializer.Deserialize<CincinnatiPostConfig>(json, opts);
|
||||||
|
|
||||||
|
Assert.Equal("CL940_CORONA", deserialized.ConfigurationName);
|
||||||
|
Assert.Equal("MS135N2PANEL.lib", deserialized.DefaultLibraryFile);
|
||||||
|
Assert.Equal(Units.Inches, deserialized.PostedUnits);
|
||||||
|
Assert.Equal(KerfMode.ControllerSide, deserialized.KerfCompensation);
|
||||||
|
Assert.True(deserialized.UseAntiDive);
|
||||||
|
|
||||||
|
// Enums serialize as strings
|
||||||
|
Assert.Contains("\"Inches\"", json);
|
||||||
|
Assert.Contains("\"ControllerSide\"", json);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParameterlessConstructor_LoadsOrCreatesConfig()
|
||||||
|
{
|
||||||
|
// The parameterless constructor reads from a .json file next to the assembly,
|
||||||
|
// or creates defaults if none exists. Either way, Config should be non-null.
|
||||||
|
var post = new CincinnatiPostProcessor();
|
||||||
|
Assert.NotNull(post.Config);
|
||||||
|
Assert.Equal("CL940", post.Config.ConfigurationName);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Post_WithPartSubprograms_WritesM98Calls()
|
||||||
|
{
|
||||||
|
var nest = CreateTestNest();
|
||||||
|
var config = new CincinnatiPostConfig
|
||||||
|
{
|
||||||
|
PostedAccuracy = 4,
|
||||||
|
UsePartSubprograms = true,
|
||||||
|
PartSubprogramStart = 200
|
||||||
|
};
|
||||||
|
var post = new CincinnatiPostProcessor(config);
|
||||||
|
|
||||||
|
using var ms = new MemoryStream();
|
||||||
|
post.Post(nest, ms);
|
||||||
|
|
||||||
|
var output = Encoding.UTF8.GetString(ms.ToArray());
|
||||||
|
|
||||||
|
// Sheet should contain M98 call to part sub-program
|
||||||
|
Assert.Contains("M98P200", output);
|
||||||
|
|
||||||
|
// Should have G92 for local coordinate positioning
|
||||||
|
Assert.Contains("G92X0Y0", output);
|
||||||
|
|
||||||
|
// Part sub-program definition
|
||||||
|
Assert.Contains(":200", output);
|
||||||
|
Assert.Contains("G84", output);
|
||||||
|
|
||||||
|
// Sub-program ends with G0X0Y0 and M99
|
||||||
|
Assert.Contains("G0X0Y0", output);
|
||||||
|
Assert.Contains("M99(END OF Square)", output);
|
||||||
|
|
||||||
|
// G92 restore after M98 call
|
||||||
|
Assert.Contains("G92X", output);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Post_WithPartSubprograms_ReusesSameSubprogram()
|
||||||
|
{
|
||||||
|
var nest = new Nest("TestNest");
|
||||||
|
var drawing = new Drawing("Square", CreateSquareProgram());
|
||||||
|
var plate = new Plate(48, 96);
|
||||||
|
plate.Parts.Add(new Part(drawing, new Vector(5, 5)));
|
||||||
|
plate.Parts.Add(new Part(drawing, new Vector(20, 5)));
|
||||||
|
nest.Plates.Add(plate);
|
||||||
|
|
||||||
|
var config = new CincinnatiPostConfig
|
||||||
|
{
|
||||||
|
PostedAccuracy = 4,
|
||||||
|
UsePartSubprograms = true,
|
||||||
|
PartSubprogramStart = 200
|
||||||
|
};
|
||||||
|
var post = new CincinnatiPostProcessor(config);
|
||||||
|
|
||||||
|
using var ms = new MemoryStream();
|
||||||
|
post.Post(nest, ms);
|
||||||
|
|
||||||
|
var output = Encoding.UTF8.GetString(ms.ToArray());
|
||||||
|
|
||||||
|
// Both parts should call the same sub-program
|
||||||
|
var m98Count = System.Text.RegularExpressions.Regex.Matches(output, "M98P200").Count;
|
||||||
|
Assert.Equal(2, m98Count);
|
||||||
|
|
||||||
|
// Only one sub-program definition
|
||||||
|
var subDefCount = System.Text.RegularExpressions.Regex.Matches(output, ":200").Count;
|
||||||
|
Assert.Equal(1, subDefCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Post_WithPartSubprograms_DifferentRotationsGetSeparateSubprograms()
|
||||||
|
{
|
||||||
|
var nest = new Nest("TestNest");
|
||||||
|
var drawing = new Drawing("Square", CreateSquareProgram());
|
||||||
|
var plate = new Plate(48, 96);
|
||||||
|
|
||||||
|
var part1 = new Part(drawing, new Vector(5, 5));
|
||||||
|
plate.Parts.Add(part1);
|
||||||
|
|
||||||
|
var part2 = new Part(drawing, new Vector(20, 5));
|
||||||
|
part2.Rotate(System.Math.PI / 2); // 90 degrees
|
||||||
|
plate.Parts.Add(part2);
|
||||||
|
|
||||||
|
nest.Plates.Add(plate);
|
||||||
|
|
||||||
|
var config = new CincinnatiPostConfig
|
||||||
|
{
|
||||||
|
PostedAccuracy = 4,
|
||||||
|
UsePartSubprograms = true,
|
||||||
|
PartSubprogramStart = 200
|
||||||
|
};
|
||||||
|
var post = new CincinnatiPostProcessor(config);
|
||||||
|
|
||||||
|
using var ms = new MemoryStream();
|
||||||
|
post.Post(nest, ms);
|
||||||
|
|
||||||
|
var output = Encoding.UTF8.GetString(ms.ToArray());
|
||||||
|
|
||||||
|
// Should have two different sub-programs
|
||||||
|
Assert.Contains(":200", output);
|
||||||
|
Assert.Contains(":201", output);
|
||||||
|
Assert.Contains("M98P200", output);
|
||||||
|
Assert.Contains("M98P201", output);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Post_WithPartSubprograms_CutoffsAreInline()
|
||||||
|
{
|
||||||
|
var nest = new Nest("TestNest");
|
||||||
|
var drawing = new Drawing("Square", CreateSquareProgram());
|
||||||
|
var cutoffDrawing = new Drawing("CutOff", CreateSquareProgram()) { IsCutOff = true };
|
||||||
|
|
||||||
|
var plate = new Plate(48, 96);
|
||||||
|
plate.Parts.Add(new Part(drawing, new Vector(5, 5)));
|
||||||
|
plate.Parts.Add(new Part(cutoffDrawing, new Vector(0, 30)));
|
||||||
|
nest.Plates.Add(plate);
|
||||||
|
|
||||||
|
var config = new CincinnatiPostConfig
|
||||||
|
{
|
||||||
|
PostedAccuracy = 4,
|
||||||
|
UsePartSubprograms = true,
|
||||||
|
PartSubprogramStart = 200
|
||||||
|
};
|
||||||
|
var post = new CincinnatiPostProcessor(config);
|
||||||
|
|
||||||
|
using var ms = new MemoryStream();
|
||||||
|
post.Post(nest, ms);
|
||||||
|
|
||||||
|
var output = Encoding.UTF8.GetString(ms.ToArray());
|
||||||
|
|
||||||
|
// Regular part uses sub-program
|
||||||
|
Assert.Contains("M98P200", output);
|
||||||
|
Assert.Contains(":200", output);
|
||||||
|
|
||||||
|
// Cutoff should NOT have its own sub-program
|
||||||
|
Assert.DoesNotContain(":201", output);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Post_WithPartSubprograms_ConfigRoundTrips()
|
||||||
|
{
|
||||||
|
var config = new CincinnatiPostConfig
|
||||||
|
{
|
||||||
|
UsePartSubprograms = true,
|
||||||
|
PartSubprogramStart = 300
|
||||||
|
};
|
||||||
|
|
||||||
|
var opts = new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
WriteIndented = true,
|
||||||
|
Converters = { new JsonStringEnumConverter() }
|
||||||
|
};
|
||||||
|
var json = JsonSerializer.Serialize(config, opts);
|
||||||
|
var deserialized = JsonSerializer.Deserialize<CincinnatiPostConfig>(json, opts);
|
||||||
|
|
||||||
|
Assert.True(deserialized.UsePartSubprograms);
|
||||||
|
Assert.Equal(300, deserialized.PartSubprogramStart);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Nest CreateTestNest()
|
||||||
|
{
|
||||||
|
var nest = new Nest("TestNest");
|
||||||
|
var drawing = new Drawing("Square", CreateSquareProgram());
|
||||||
|
nest.Drawings.Add(drawing);
|
||||||
|
|
||||||
|
var plate = new Plate(48.0, 96.0);
|
||||||
|
plate.Parts.Add(new Part(drawing, new Vector(10, 10)));
|
||||||
|
nest.Plates.Add(plate);
|
||||||
|
|
||||||
|
return nest;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Program CreateSquareProgram()
|
||||||
|
{
|
||||||
|
var pgm = new Program();
|
||||||
|
pgm.Codes.Add(new RapidMove(0, 0));
|
||||||
|
pgm.Codes.Add(new LinearMove(2, 0));
|
||||||
|
pgm.Codes.Add(new LinearMove(2, 2));
|
||||||
|
pgm.Codes.Add(new LinearMove(0, 2));
|
||||||
|
pgm.Codes.Add(new LinearMove(0, 0));
|
||||||
|
return pgm;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using OpenNest.CNC;
|
||||||
|
using OpenNest.Posts.Cincinnati;
|
||||||
|
|
||||||
|
namespace OpenNest.Tests.Cincinnati;
|
||||||
|
|
||||||
|
public class CincinnatiPreambleWriterTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void WriteMainProgram_EmitsHeader()
|
||||||
|
{
|
||||||
|
var config = new CincinnatiPostConfig
|
||||||
|
{
|
||||||
|
ConfigurationName = "CL940",
|
||||||
|
PostedUnits = Units.Inches,
|
||||||
|
DefaultLibraryFile = "MS135N2PANEL.lib"
|
||||||
|
};
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
using var sw = new StringWriter(sb);
|
||||||
|
var writer = new CincinnatiPreambleWriter(config);
|
||||||
|
|
||||||
|
writer.WriteMainProgram(sw, "TestNest", "Mild Steel, 10GA", 2);
|
||||||
|
|
||||||
|
var output = sb.ToString();
|
||||||
|
Assert.Contains("( NEST TestNest )", output);
|
||||||
|
Assert.Contains("( CONFIGURATION - CL940 )", output);
|
||||||
|
Assert.Contains("G20", output);
|
||||||
|
Assert.Contains("M42", output);
|
||||||
|
Assert.Contains("G89 P MS135N2PANEL.lib", output);
|
||||||
|
Assert.Contains("M98 P100 (Variable Declaration)", output);
|
||||||
|
Assert.Contains("GOTO1 (GOTO SHEET NUMBER)", output);
|
||||||
|
Assert.Contains("N1M98 P101 (SHEET 1)", output);
|
||||||
|
Assert.Contains("N2M98 P102 (SHEET 2)", output);
|
||||||
|
Assert.Contains("M30 (END OF MAIN)", output);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WriteMainProgram_EmitsG21ForMetric()
|
||||||
|
{
|
||||||
|
var config = new CincinnatiPostConfig { PostedUnits = Units.Millimeters };
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
using var sw = new StringWriter(sb);
|
||||||
|
var writer = new CincinnatiPreambleWriter(config);
|
||||||
|
|
||||||
|
writer.WriteMainProgram(sw, "Test", "", 1);
|
||||||
|
|
||||||
|
Assert.Contains("G21", sb.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WriteMainProgram_EmitsG61_WhenExactStop()
|
||||||
|
{
|
||||||
|
var config = new CincinnatiPostConfig { UseExactStopMode = true };
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
using var sw = new StringWriter(sb);
|
||||||
|
var writer = new CincinnatiPreambleWriter(config);
|
||||||
|
|
||||||
|
writer.WriteMainProgram(sw, "Test", "", 1);
|
||||||
|
|
||||||
|
Assert.Contains("G61", sb.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WriteMainProgram_OmitsG61_WhenNotExactStop()
|
||||||
|
{
|
||||||
|
var config = new CincinnatiPostConfig { UseExactStopMode = false };
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
using var sw = new StringWriter(sb);
|
||||||
|
var writer = new CincinnatiPreambleWriter(config);
|
||||||
|
|
||||||
|
writer.WriteMainProgram(sw, "Test", "", 1);
|
||||||
|
|
||||||
|
Assert.DoesNotContain("G61", sb.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WriteVariableDeclaration_EmitsSubprogram()
|
||||||
|
{
|
||||||
|
var config = new CincinnatiPostConfig();
|
||||||
|
var vars = new ProgramVariableManager();
|
||||||
|
vars.GetOrCreate("LeadInFeedrate", 126, "[#148*0.5]");
|
||||||
|
vars.GetOrCreate("CircleFeedrate", 128, ".8");
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
using var sw = new StringWriter(sb);
|
||||||
|
var writer = new CincinnatiPreambleWriter(config);
|
||||||
|
|
||||||
|
writer.WriteVariableDeclaration(sw, vars);
|
||||||
|
|
||||||
|
var output = sb.ToString();
|
||||||
|
Assert.Contains(":100", output);
|
||||||
|
Assert.Contains("(Variable Declaration Start)", output);
|
||||||
|
Assert.Contains("#126=", output);
|
||||||
|
Assert.Contains("#128=", output);
|
||||||
|
Assert.Contains("M99 (Variable Declaration End)", output);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using OpenNest.CNC;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using OpenNest.Posts.Cincinnati;
|
||||||
|
|
||||||
|
namespace OpenNest.Tests.Cincinnati;
|
||||||
|
|
||||||
|
public class CincinnatiSheetWriterTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void WriteSheet_EmitsSheetHeader()
|
||||||
|
{
|
||||||
|
var config = new CincinnatiPostConfig
|
||||||
|
{
|
||||||
|
DefaultLibraryFile = "MS135N2PANEL.lib",
|
||||||
|
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.Contains(":101", output);
|
||||||
|
Assert.Contains("( Sheet 1 )", output);
|
||||||
|
Assert.Contains("#110=", output);
|
||||||
|
Assert.Contains("#111=", output);
|
||||||
|
Assert.Contains("G92X#5021Y#5022", output);
|
||||||
|
Assert.Contains("M99", output);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WriteSheet_EmitsReturnToOriginAndPalletExchange()
|
||||||
|
{
|
||||||
|
var config = new CincinnatiPostConfig
|
||||||
|
{
|
||||||
|
PalletExchange = PalletMode.EndOfSheet,
|
||||||
|
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.Contains("M42", output);
|
||||||
|
Assert.Contains("G0X0Y0", output);
|
||||||
|
Assert.Contains("M50", output);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WriteSheet_SkipsEmptyPlate()
|
||||||
|
{
|
||||||
|
var config = new CincinnatiPostConfig { PostedAccuracy = 4 };
|
||||||
|
var plate = new Plate(48.0, 96.0);
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
using var sw = new StringWriter(sb);
|
||||||
|
var sheetWriter = new CincinnatiSheetWriter(config, new ProgramVariableManager());
|
||||||
|
|
||||||
|
sheetWriter.Write(sw, plate, "TestNest", 1, 101);
|
||||||
|
|
||||||
|
Assert.Equal("", sb.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WriteSheet_SplitsMultiContourParts()
|
||||||
|
{
|
||||||
|
var config = new CincinnatiPostConfig { PostedAccuracy = 4 };
|
||||||
|
var pgm = new Program();
|
||||||
|
// First contour (hole)
|
||||||
|
pgm.Codes.Add(new RapidMove(1, 1));
|
||||||
|
pgm.Codes.Add(new LinearMove(2, 1));
|
||||||
|
pgm.Codes.Add(new LinearMove(2, 2));
|
||||||
|
pgm.Codes.Add(new LinearMove(1, 1));
|
||||||
|
// Second contour (exterior)
|
||||||
|
pgm.Codes.Add(new RapidMove(0, 0));
|
||||||
|
pgm.Codes.Add(new LinearMove(5, 0));
|
||||||
|
pgm.Codes.Add(new LinearMove(5, 5));
|
||||||
|
pgm.Codes.Add(new LinearMove(0, 0));
|
||||||
|
|
||||||
|
var plate = new Plate(48.0, 96.0);
|
||||||
|
plate.Parts.Add(new Part(new Drawing("MultiContour", pgm)));
|
||||||
|
|
||||||
|
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();
|
||||||
|
// Should have two G84 pierce commands (one per contour)
|
||||||
|
var g84Count = output.Split('\n').Count(l => l.Trim() == "G84");
|
||||||
|
Assert.Equal(2, g84Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Program CreateSimpleProgram()
|
||||||
|
{
|
||||||
|
var pgm = new Program();
|
||||||
|
pgm.Codes.Add(new RapidMove(0, 0));
|
||||||
|
pgm.Codes.Add(new LinearMove(1, 0));
|
||||||
|
pgm.Codes.Add(new LinearMove(1, 1));
|
||||||
|
pgm.Codes.Add(new LinearMove(0, 1));
|
||||||
|
pgm.Codes.Add(new LinearMove(0, 0));
|
||||||
|
return pgm;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using OpenNest.Posts.Cincinnati;
|
using OpenNest.CNC;
|
||||||
|
|
||||||
namespace OpenNest.Tests.Cincinnati;
|
namespace OpenNest.Tests.Cincinnati;
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ public class SpeedClassifierTests
|
|||||||
{
|
{
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(20.0, 10.0, "FAST")]
|
[InlineData(20.0, 10.0, "FAST")]
|
||||||
[InlineData(5.0, 10.0, "MEDIUM")]
|
[InlineData(5.0, 10.0, "FAST")]
|
||||||
|
[InlineData(4.9, 10.0, "MEDIUM")]
|
||||||
[InlineData(0.5, 10.0, "SLOW")]
|
[InlineData(0.5, 10.0, "SLOW")]
|
||||||
public void Classify_ReturnsExpectedClass(double contourLength, double sheetDiagonal, string expected)
|
public void Classify_ReturnsExpectedClass(double contourLength, double sheetDiagonal, string expected)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ namespace OpenNest.Controls
|
|||||||
private CutOffSettings cutOffSettings = new CutOffSettings();
|
private CutOffSettings cutOffSettings = new CutOffSettings();
|
||||||
private CutOff selectedCutOff;
|
private CutOff selectedCutOff;
|
||||||
private bool draggingCutOff;
|
private bool draggingCutOff;
|
||||||
|
private Dictionary<Part, Geometry.Entity> dragPerimeterCache;
|
||||||
protected List<LayoutPart> parts;
|
protected List<LayoutPart> parts;
|
||||||
private List<LayoutPart> stationaryParts = new List<LayoutPart>();
|
private List<LayoutPart> stationaryParts = new List<LayoutPart>();
|
||||||
private List<LayoutPart> activeParts = new List<LayoutPart>();
|
private List<LayoutPart> activeParts = new List<LayoutPart>();
|
||||||
@@ -242,6 +243,7 @@ namespace OpenNest.Controls
|
|||||||
{
|
{
|
||||||
SelectedCutOff = hitCutOff;
|
SelectedCutOff = hitCutOff;
|
||||||
draggingCutOff = true;
|
draggingCutOff = true;
|
||||||
|
dragPerimeterCache = Plate.BuildPerimeterCache(Plate);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -270,6 +272,7 @@ namespace OpenNest.Controls
|
|||||||
if (draggingCutOff && selectedCutOff != null)
|
if (draggingCutOff && selectedCutOff != null)
|
||||||
{
|
{
|
||||||
draggingCutOff = false;
|
draggingCutOff = false;
|
||||||
|
dragPerimeterCache = null;
|
||||||
Plate.RegenerateCutOffs(cutOffSettings);
|
Plate.RegenerateCutOffs(cutOffSettings);
|
||||||
Invalidate();
|
Invalidate();
|
||||||
return;
|
return;
|
||||||
@@ -333,7 +336,12 @@ namespace OpenNest.Controls
|
|||||||
|
|
||||||
if (draggingCutOff && selectedCutOff != null)
|
if (draggingCutOff && selectedCutOff != null)
|
||||||
{
|
{
|
||||||
selectedCutOff.Position = CurrentPoint;
|
if (selectedCutOff.Axis == CutOffAxis.Vertical)
|
||||||
|
selectedCutOff.Position = new Vector(CurrentPoint.X, selectedCutOff.Position.Y);
|
||||||
|
else
|
||||||
|
selectedCutOff.Position = new Vector(selectedCutOff.Position.X, CurrentPoint.Y);
|
||||||
|
|
||||||
|
selectedCutOff.Regenerate(Plate, cutOffSettings, dragPerimeterCache);
|
||||||
Invalidate();
|
Invalidate();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -455,7 +463,6 @@ namespace OpenNest.Controls
|
|||||||
DrawPlate(e.Graphics);
|
DrawPlate(e.Graphics);
|
||||||
DrawParts(e.Graphics);
|
DrawParts(e.Graphics);
|
||||||
DrawCutOffs(e.Graphics);
|
DrawCutOffs(e.Graphics);
|
||||||
DrawCutOffGrip(e.Graphics);
|
|
||||||
DrawActiveWorkArea(e.Graphics);
|
DrawActiveWorkArea(e.Graphics);
|
||||||
DrawDebugRemnants(e.Graphics);
|
DrawDebugRemnants(e.Graphics);
|
||||||
|
|
||||||
@@ -612,7 +619,8 @@ namespace OpenNest.Controls
|
|||||||
if (Plate?.CutOffs == null || Plate.CutOffs.Count == 0)
|
if (Plate?.CutOffs == null || Plate.CutOffs.Count == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
using var pen = new Pen(Color.FromArgb(64, 64, 64), 1.5f / ViewScale);
|
using var pen = new Pen(Color.FromArgb(64, 64, 64), 1.5f);
|
||||||
|
using var selectedPen = new Pen(Color.FromArgb(0, 120, 255), 3.5f);
|
||||||
|
|
||||||
foreach (var cutoff in Plate.CutOffs)
|
foreach (var cutoff in Plate.CutOffs)
|
||||||
{
|
{
|
||||||
@@ -620,35 +628,19 @@ namespace OpenNest.Controls
|
|||||||
if (program == null || program.Codes.Count == 0)
|
if (program == null || program.Codes.Count == 0)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
var activePen = cutoff == selectedCutOff ? selectedPen : pen;
|
||||||
|
|
||||||
for (var i = 0; i < program.Codes.Count - 1; i += 2)
|
for (var i = 0; i < program.Codes.Count - 1; i += 2)
|
||||||
{
|
{
|
||||||
if (program.Codes[i] is RapidMove rapid &&
|
if (program.Codes[i] is RapidMove rapid &&
|
||||||
program.Codes[i + 1] is LinearMove linear)
|
program.Codes[i + 1] is LinearMove linear)
|
||||||
{
|
{
|
||||||
DrawLine(g, rapid.EndPoint, linear.EndPoint, pen);
|
DrawLine(g, rapid.EndPoint, linear.EndPoint, activePen);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawCutOffGrip(Graphics g)
|
|
||||||
{
|
|
||||||
if (selectedCutOff == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var radius = 4f / ViewScale;
|
|
||||||
var pos = selectedCutOff.Position;
|
|
||||||
var graphPt = PointWorldToGraph(pos);
|
|
||||||
var scaledRadius = LengthWorldToGui(radius);
|
|
||||||
|
|
||||||
g.FillEllipse(Brushes.DarkGray,
|
|
||||||
graphPt.X - scaledRadius, graphPt.Y - scaledRadius,
|
|
||||||
scaledRadius * 2, scaledRadius * 2);
|
|
||||||
g.DrawEllipse(Pens.Black,
|
|
||||||
graphPt.X - scaledRadius, graphPt.Y - scaledRadius,
|
|
||||||
scaledRadius * 2, scaledRadius * 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
public CutOff GetCutOffAtPoint(Vector point, double tolerance)
|
public CutOff GetCutOffAtPoint(Vector point, double tolerance)
|
||||||
{
|
{
|
||||||
if (Plate?.CutOffs == null)
|
if (Plate?.CutOffs == null)
|
||||||
@@ -656,9 +648,20 @@ namespace OpenNest.Controls
|
|||||||
|
|
||||||
foreach (var cutoff in Plate.CutOffs)
|
foreach (var cutoff in Plate.CutOffs)
|
||||||
{
|
{
|
||||||
var dist = cutoff.Position.DistanceTo(point);
|
var program = cutoff.Drawing?.Program;
|
||||||
if (dist <= tolerance)
|
if (program == null)
|
||||||
return cutoff;
|
continue;
|
||||||
|
|
||||||
|
for (var i = 0; i < program.Codes.Count - 1; i += 2)
|
||||||
|
{
|
||||||
|
if (program.Codes[i] is RapidMove rapid &&
|
||||||
|
program.Codes[i + 1] is LinearMove linear)
|
||||||
|
{
|
||||||
|
var line = new Geometry.Line(rapid.EndPoint, linear.EndPoint);
|
||||||
|
if (line.ClosestPointTo(point).DistanceTo(point) <= tolerance)
|
||||||
|
return cutoff;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
Reference in New Issue
Block a user