Compare commits
10 Commits
95a0db1983
...
833abfe72e
| 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
|
||||
{
|
||||
+1
-1
@@ -2,7 +2,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace OpenNest.Posts.Cincinnati
|
||||
namespace OpenNest.CNC
|
||||
{
|
||||
public sealed class ProgramVariableManager
|
||||
{
|
||||
+22
-2
@@ -129,9 +129,11 @@ namespace OpenNest
|
||||
private List<(double Start, double End)> IntersectPerimeter(
|
||||
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));
|
||||
|
||||
if (!perimeter.Intersects(cutLine, out var pts) || pts.Count < 2)
|
||||
if (!target.Intersects(cutLine, out var pts) || pts.Count < 2)
|
||||
return null;
|
||||
|
||||
var coords = pts
|
||||
@@ -142,13 +144,31 @@ namespace OpenNest
|
||||
if (coords.Count % 2 != 0)
|
||||
return null;
|
||||
|
||||
var padding = usedOffset ? 0 : clearance;
|
||||
var result = new List<(double Start, double End)>();
|
||||
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;
|
||||
}
|
||||
|
||||
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) =>
|
||||
Axis == CutOffAxis.Vertical
|
||||
? new Vector(cutCoord, lineCoord)
|
||||
|
||||
@@ -317,12 +317,68 @@ namespace OpenNest.Geometry
|
||||
|
||||
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)
|
||||
{
|
||||
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>
|
||||
|
||||
@@ -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.
|
||||
/// Defines machine-specific parameters, output format, and cutting strategies.
|
||||
/// </summary>
|
||||
public class CincinnatiPostConfig
|
||||
public sealed class CincinnatiPostConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the configuration name/identifier.
|
||||
@@ -126,6 +126,20 @@ namespace OpenNest.Posts.Cincinnati
|
||||
/// </summary>
|
||||
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>
|
||||
/// Gets or sets the subprogram number for variable declarations.
|
||||
/// 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)
|
||||
{
|
||||
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} )";
|
||||
|
||||
@@ -6,4 +6,11 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\OpenNest.Core\OpenNest.Core.csproj" />
|
||||
</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>
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace OpenNest.Posts.Cincinnati
|
||||
public string Classify(double contourLength, double sheetDiagonal)
|
||||
{
|
||||
var ratio = contourLength / sheetDiagonal;
|
||||
if (ratio > FastThreshold) return "FAST";
|
||||
if (ratio >= FastThreshold) return "FAST";
|
||||
if (ratio <= SlowThreshold) return "SLOW";
|
||||
return "MEDIUM";
|
||||
}
|
||||
@@ -22,7 +22,7 @@ namespace OpenNest.Posts.Cincinnati
|
||||
{
|
||||
// Cincinnati convention: no leading zero for values < 1 (e.g., ".8702" not "0.8702")
|
||||
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."))
|
||||
return str.Substring(1);
|
||||
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;
|
||||
|
||||
|
||||
@@ -6,7 +6,8 @@ public class SpeedClassifierTests
|
||||
{
|
||||
[Theory]
|
||||
[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")]
|
||||
public void Classify_ReturnsExpectedClass(double contourLength, double sheetDiagonal, string expected)
|
||||
{
|
||||
|
||||
@@ -33,6 +33,7 @@ namespace OpenNest.Controls
|
||||
private CutOffSettings cutOffSettings = new CutOffSettings();
|
||||
private CutOff selectedCutOff;
|
||||
private bool draggingCutOff;
|
||||
private Dictionary<Part, Geometry.Entity> dragPerimeterCache;
|
||||
protected List<LayoutPart> parts;
|
||||
private List<LayoutPart> stationaryParts = new List<LayoutPart>();
|
||||
private List<LayoutPart> activeParts = new List<LayoutPart>();
|
||||
@@ -242,6 +243,7 @@ namespace OpenNest.Controls
|
||||
{
|
||||
SelectedCutOff = hitCutOff;
|
||||
draggingCutOff = true;
|
||||
dragPerimeterCache = Plate.BuildPerimeterCache(Plate);
|
||||
return;
|
||||
}
|
||||
else
|
||||
@@ -270,6 +272,7 @@ namespace OpenNest.Controls
|
||||
if (draggingCutOff && selectedCutOff != null)
|
||||
{
|
||||
draggingCutOff = false;
|
||||
dragPerimeterCache = null;
|
||||
Plate.RegenerateCutOffs(cutOffSettings);
|
||||
Invalidate();
|
||||
return;
|
||||
@@ -333,7 +336,12 @@ namespace OpenNest.Controls
|
||||
|
||||
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();
|
||||
return;
|
||||
}
|
||||
@@ -455,7 +463,6 @@ namespace OpenNest.Controls
|
||||
DrawPlate(e.Graphics);
|
||||
DrawParts(e.Graphics);
|
||||
DrawCutOffs(e.Graphics);
|
||||
DrawCutOffGrip(e.Graphics);
|
||||
DrawActiveWorkArea(e.Graphics);
|
||||
DrawDebugRemnants(e.Graphics);
|
||||
|
||||
@@ -612,7 +619,8 @@ namespace OpenNest.Controls
|
||||
if (Plate?.CutOffs == null || Plate.CutOffs.Count == 0)
|
||||
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)
|
||||
{
|
||||
@@ -620,35 +628,19 @@ namespace OpenNest.Controls
|
||||
if (program == null || program.Codes.Count == 0)
|
||||
continue;
|
||||
|
||||
var activePen = cutoff == selectedCutOff ? selectedPen : pen;
|
||||
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (Plate?.CutOffs == null)
|
||||
@@ -656,9 +648,20 @@ namespace OpenNest.Controls
|
||||
|
||||
foreach (var cutoff in Plate.CutOffs)
|
||||
{
|
||||
var dist = cutoff.Position.DistanceTo(point);
|
||||
if (dist <= tolerance)
|
||||
return cutoff;
|
||||
var program = cutoff.Drawing?.Program;
|
||||
if (program == null)
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user