fix: Cincinnati post processor arc feedrate, G89 spacing, pallet exchange, and preamble
- Add radius-based arc feedrate calculation (Variables/Percentages modes) with configurable radius ranges (#123/#124/#125 or inline expressions) - Fix arc distance in SpeedClassifier using actual arc length instead of chord length (full circles previously computed as zero) - Fix G89 P spacing: P now adjacent to filename per CL-707 manual syntax - Add lead-out feedrate support (#129) and arc lead-in feedrate (#127) - Fix pallet exchange: StartAndEnd emits M50 in preamble + last sheet only - Add G121 Smart Rapids emission when UseSmartRapids is enabled - Add G90 absolute mode to main program preamble alongside G20/G21 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -70,7 +70,7 @@ public sealed class CincinnatiFeatureWriter
|
||||
{
|
||||
var speedClass = _speedClassifier.Classify(ctx.CutDistance, ctx.SheetDiagonal);
|
||||
var cutDist = _speedClassifier.FormatCutDist(ctx.CutDistance, ctx.SheetDiagonal);
|
||||
writer.WriteLine($"G89 P {lib} ({speedClass} {cutDist})");
|
||||
writer.WriteLine($"G89 P{lib} ({speedClass} {cutDist})");
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -108,7 +108,7 @@ public sealed class CincinnatiFeatureWriter
|
||||
sb.Append($"G1 X{_fmt.FormatCoord(linear.EndPoint.X)} Y{_fmt.FormatCoord(linear.EndPoint.Y)}");
|
||||
|
||||
// Feedrate — etch always uses process feedrate
|
||||
var feedVar = ctx.IsEtch ? "#148" : GetFeedVariable(linear.Layer);
|
||||
var feedVar = ctx.IsEtch ? "#148" : GetLinearFeedVariable(linear.Layer);
|
||||
if (feedVar != lastFeedVar)
|
||||
{
|
||||
sb.Append($" F{feedVar}");
|
||||
@@ -138,11 +138,11 @@ public sealed class CincinnatiFeatureWriter
|
||||
var j = arc.CenterPoint.Y - currentPos.Y;
|
||||
sb.Append($" I{_fmt.FormatCoord(i)} J{_fmt.FormatCoord(j)}");
|
||||
|
||||
// Feedrate — etch always uses process feedrate, cut uses layer-based
|
||||
// Feedrate — etch always uses process feedrate, cut uses layer/radius-based
|
||||
var radius = currentPos.DistanceTo(arc.CenterPoint);
|
||||
var isFullCircle = IsFullCircle(currentPos, arc.EndPoint);
|
||||
var feedVar = ctx.IsEtch ? "#148"
|
||||
: isFullCircle ? "[#148*#128]"
|
||||
: GetFeedVariable(arc.Layer);
|
||||
: GetArcFeedrate(arc.Layer, radius, isFullCircle);
|
||||
if (feedVar != lastFeedVar)
|
||||
{
|
||||
sb.Append($" F{feedVar}");
|
||||
@@ -223,16 +223,45 @@ public sealed class CincinnatiFeatureWriter
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetFeedVariable(LayerType layer)
|
||||
private static string GetLinearFeedVariable(LayerType layer)
|
||||
{
|
||||
return layer switch
|
||||
{
|
||||
LayerType.Leadin => "#126",
|
||||
LayerType.Cut => "#148",
|
||||
LayerType.Leadout => "#129",
|
||||
_ => "#148"
|
||||
};
|
||||
}
|
||||
|
||||
private string GetArcFeedrate(LayerType layer, double radius, bool isFullCircle)
|
||||
{
|
||||
if (layer == LayerType.Leadin) return "#127";
|
||||
if (layer == LayerType.Leadout) return "#129";
|
||||
if (isFullCircle) return "[#148*#128]";
|
||||
return GetArcCutFeedrate(radius);
|
||||
}
|
||||
|
||||
private string GetArcCutFeedrate(double radius)
|
||||
{
|
||||
if (_config.ArcFeedrate == ArcFeedrateMode.None)
|
||||
return "#148";
|
||||
|
||||
// Find the smallest range that contains this radius
|
||||
ArcFeedrateRange best = null;
|
||||
foreach (var range in _config.ArcFeedrateRanges)
|
||||
{
|
||||
if (radius <= range.MaxRadius && (best == null || range.MaxRadius < best.MaxRadius))
|
||||
best = range;
|
||||
}
|
||||
|
||||
if (best == null)
|
||||
return "#148";
|
||||
|
||||
return _config.ArcFeedrate == ArcFeedrateMode.Variables
|
||||
? $"#{best.VariableNumber}"
|
||||
: $"[#148*{best.FeedratePercent.ToString("0.##", System.Globalization.CultureInfo.InvariantCulture)}]";
|
||||
}
|
||||
|
||||
private static bool IsFullCircle(Vector start, Vector end)
|
||||
{
|
||||
return Tolerance.IsEqualTo(start.X, end.X) && Tolerance.IsEqualTo(start.Y, end.Y);
|
||||
|
||||
@@ -269,12 +269,35 @@ namespace OpenNest.Posts.Cincinnati
|
||||
/// </summary>
|
||||
public double LeadInArcLine2FeedratePercent { get; set; } = 0.5;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the feedrate percentage for lead-out moves.
|
||||
/// Default: 0.5 (50%)
|
||||
/// </summary>
|
||||
public double LeadOutFeedratePercent { get; set; } = 0.5;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the feedrate multiplier for circular cuts.
|
||||
/// Default: 0.8 (80%)
|
||||
/// </summary>
|
||||
public double CircleFeedrateMultiplier { get; set; } = 0.8;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the arc feedrate calculation mode.
|
||||
/// Default: ArcFeedrateMode.None
|
||||
/// </summary>
|
||||
public ArcFeedrateMode ArcFeedrate { get; set; } = ArcFeedrateMode.None;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the radius-based arc feedrate ranges.
|
||||
/// Ranges are matched from smallest MaxRadius to largest.
|
||||
/// </summary>
|
||||
public List<ArcFeedrateRange> ArcFeedrateRanges { get; set; } = new()
|
||||
{
|
||||
new() { MaxRadius = 0.125, FeedratePercent = 0.25, VariableNumber = 123 },
|
||||
new() { MaxRadius = 0.750, FeedratePercent = 0.50, VariableNumber = 124 },
|
||||
new() { MaxRadius = 4.500, FeedratePercent = 0.80, VariableNumber = 125 }
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the variable number for sheet width.
|
||||
/// Default: 110
|
||||
@@ -301,4 +324,34 @@ namespace OpenNest.Posts.Cincinnati
|
||||
public string Gas { get; set; } = "";
|
||||
public string Library { get; set; } = "";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies how arc feedrates are calculated based on radius.
|
||||
/// </summary>
|
||||
public enum ArcFeedrateMode
|
||||
{
|
||||
/// <summary>No radius-based arc feedrate adjustment (only full circles use multiplier).</summary>
|
||||
None,
|
||||
|
||||
/// <summary>Inline percentage expressions: F [#148*pct] based on radius range.</summary>
|
||||
Percentages,
|
||||
|
||||
/// <summary>Radius-range-based variables: F #varNum based on radius range.</summary>
|
||||
Variables
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines a radius range and its associated feedrate for arc moves.
|
||||
/// </summary>
|
||||
public class ArcFeedrateRange
|
||||
{
|
||||
/// <summary>Maximum radius for this range (inclusive).</summary>
|
||||
public double MaxRadius { get; set; }
|
||||
|
||||
/// <summary>Feedrate as a fraction of process feedrate (e.g. 0.25 = 25%).</summary>
|
||||
public double FeedratePercent { get; set; }
|
||||
|
||||
/// <summary>Variable number for Variables mode (e.g. 123).</summary>
|
||||
public int VariableNumber { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,8 +112,9 @@ namespace OpenNest.Posts.Cincinnati
|
||||
var sheetIndex = i + 1;
|
||||
var subNumber = Config.SheetSubprogramStart + i;
|
||||
var cutLibrary = resolver.ResolveCutLibrary(plate.Material?.Name ?? "", plate.Thickness, gas);
|
||||
var isLastSheet = i == plates.Count - 1;
|
||||
sheetWriter.Write(writer, plate, nest.Name ?? "NEST", sheetIndex, subNumber,
|
||||
cutLibrary, etchLibrary, partSubprograms);
|
||||
cutLibrary, etchLibrary, partSubprograms, isLastSheet);
|
||||
}
|
||||
|
||||
// Part sub-programs (if enabled)
|
||||
@@ -148,6 +149,18 @@ namespace OpenNest.Posts.Cincinnati
|
||||
vars.GetOrCreate("LeadInFeedrate", 126, $"[#148*{Config.LeadInFeedratePercent}]");
|
||||
vars.GetOrCreate("LeadInArcLine2Feedrate", 127, $"[#148*{Config.LeadInArcLine2FeedratePercent}]");
|
||||
vars.GetOrCreate("CircleFeedrate", 128, Config.CircleFeedrateMultiplier.ToString("0.#"));
|
||||
vars.GetOrCreate("LeadOutFeedrate", 129, $"[#148*{Config.LeadOutFeedratePercent}]");
|
||||
|
||||
if (Config.ArcFeedrate == ArcFeedrateMode.Variables)
|
||||
{
|
||||
foreach (var range in Config.ArcFeedrateRanges)
|
||||
{
|
||||
var name = $"ArcFeedR{range.MaxRadius.ToString("0.###", System.Globalization.CultureInfo.InvariantCulture)}";
|
||||
vars.GetOrCreate(name, range.VariableNumber,
|
||||
$"[#148*{range.FeedratePercent.ToString("0.##", System.Globalization.CultureInfo.InvariantCulture)}]");
|
||||
}
|
||||
}
|
||||
|
||||
return vars;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,15 +37,21 @@ public sealed class CincinnatiPreambleWriter
|
||||
|
||||
w.WriteLine(CoordinateFormatter.Comment("MAIN PROGRAM"));
|
||||
|
||||
w.WriteLine(_config.PostedUnits == Units.Millimeters ? "G21" : "G20");
|
||||
w.WriteLine(_config.PostedUnits == Units.Millimeters ? "G21 G90" : "G20 G90");
|
||||
|
||||
if (_config.UseSmartRapids)
|
||||
w.WriteLine("G121 (SMART RAPIDS)");
|
||||
|
||||
w.WriteLine("M42");
|
||||
|
||||
if (_config.ProcessParameterMode == G89Mode.LibraryFile && !string.IsNullOrEmpty(initialLibrary))
|
||||
w.WriteLine($"G89 P {initialLibrary}");
|
||||
w.WriteLine($"G89 P{initialLibrary}");
|
||||
|
||||
w.WriteLine($"M98 P{_config.VariableDeclarationSubprogram} (Variable Declaration)");
|
||||
|
||||
if (_config.PalletExchange == PalletMode.StartAndEnd)
|
||||
w.WriteLine("M50");
|
||||
|
||||
w.WriteLine("GOTO1 (GOTO SHEET NUMBER)");
|
||||
|
||||
for (var i = 1; i <= sheetCount; i++)
|
||||
|
||||
@@ -37,7 +37,8 @@ public sealed class CincinnatiSheetWriter
|
||||
/// </param>
|
||||
public void Write(TextWriter w, Plate plate, string nestName, int sheetIndex, int subNumber,
|
||||
string cutLibrary, string etchLibrary,
|
||||
Dictionary<(int, long), int> partSubprograms = null)
|
||||
Dictionary<(int, long), int> partSubprograms = null,
|
||||
bool isLastSheet = false)
|
||||
{
|
||||
if (plate.Parts.Count == 0)
|
||||
return;
|
||||
@@ -64,12 +65,12 @@ public sealed class CincinnatiSheetWriter
|
||||
w.WriteLine("N10000");
|
||||
w.WriteLine("G92 X#5021 Y#5022");
|
||||
if (!string.IsNullOrEmpty(cutLibrary))
|
||||
w.WriteLine($"G89 P {cutLibrary}");
|
||||
w.WriteLine($"G89 P{cutLibrary}");
|
||||
w.WriteLine($"M98 P{varDeclSub} (Variable Declaration)");
|
||||
w.WriteLine("G90");
|
||||
w.WriteLine("M47");
|
||||
if (!string.IsNullOrEmpty(cutLibrary))
|
||||
w.WriteLine($"G89 P {cutLibrary}");
|
||||
w.WriteLine($"G89 P{cutLibrary}");
|
||||
w.WriteLine("GOTO1( Goto Feature )");
|
||||
|
||||
// 3. Order parts: non-cutoff sorted by Bottom then Left, cutoffs last
|
||||
@@ -94,7 +95,9 @@ public sealed class CincinnatiSheetWriter
|
||||
// 5. Footer
|
||||
w.WriteLine("M42");
|
||||
w.WriteLine("G0 X0 Y0");
|
||||
if (_config.PalletExchange != PalletMode.None)
|
||||
var emitM50 = _config.PalletExchange == PalletMode.EndOfSheet
|
||||
|| (_config.PalletExchange == PalletMode.StartAndEnd && isLastSheet);
|
||||
if (emitM50)
|
||||
w.WriteLine($"N{sheetIndex + 1} M50");
|
||||
w.WriteLine($"M99 (END OF {nestName}.{sheetIndex:D3})");
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.Posts.Cincinnati;
|
||||
|
||||
@@ -105,11 +106,48 @@ public static class FeatureUtils
|
||||
}
|
||||
else if (code is ArcMove arc)
|
||||
{
|
||||
distance += currentPos.DistanceTo(arc.EndPoint);
|
||||
distance += ComputeArcLength(currentPos, arc);
|
||||
currentPos = arc.EndPoint;
|
||||
}
|
||||
}
|
||||
|
||||
return distance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the arc length from the current position through an arc move.
|
||||
/// Uses radius * sweep angle instead of chord length.
|
||||
/// </summary>
|
||||
public static double ComputeArcLength(Vector startPos, ArcMove arc)
|
||||
{
|
||||
var radius = startPos.DistanceTo(arc.CenterPoint);
|
||||
if (radius < Tolerance.Epsilon)
|
||||
return 0.0;
|
||||
|
||||
// Full circle: start ≈ end
|
||||
if (Tolerance.IsEqualTo(startPos.X, arc.EndPoint.X)
|
||||
&& Tolerance.IsEqualTo(startPos.Y, arc.EndPoint.Y))
|
||||
return 2.0 * System.Math.PI * radius;
|
||||
|
||||
var startAngle = System.Math.Atan2(
|
||||
startPos.Y - arc.CenterPoint.Y,
|
||||
startPos.X - arc.CenterPoint.X);
|
||||
var endAngle = System.Math.Atan2(
|
||||
arc.EndPoint.Y - arc.CenterPoint.Y,
|
||||
arc.EndPoint.X - arc.CenterPoint.X);
|
||||
|
||||
double sweep;
|
||||
if (arc.Rotation == RotationType.CW)
|
||||
{
|
||||
sweep = startAngle - endAngle;
|
||||
if (sweep <= 0) sweep += 2.0 * System.Math.PI;
|
||||
}
|
||||
else
|
||||
{
|
||||
sweep = endAngle - startAngle;
|
||||
if (sweep <= 0) sweep += 2.0 * System.Math.PI;
|
||||
}
|
||||
|
||||
return radius * sweep;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,7 +297,7 @@ public class CincinnatiFeatureWriterTests
|
||||
ctx.SheetDiagonal = 30.0;
|
||||
var output = WriteFeature(config, ctx);
|
||||
|
||||
Assert.Contains("G89 P MILD10", output);
|
||||
Assert.Contains("G89 PMILD10", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -470,6 +470,123 @@ public class CincinnatiFeatureWriterTests
|
||||
Assert.True(m131Idx < m47Idx, "M131 should come before M47");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LeadoutFeedrate_UsesVariable129()
|
||||
{
|
||||
var config = DefaultConfig();
|
||||
config.KerfCompensation = KerfMode.PreApplied;
|
||||
var codes = new List<ICode>
|
||||
{
|
||||
new RapidMove(1.0, 1.0),
|
||||
new LinearMove(2.0, 1.0) { Layer = LayerType.Leadout }
|
||||
};
|
||||
var ctx = SimpleContext(codes);
|
||||
var output = WriteFeature(config, ctx);
|
||||
|
||||
Assert.Contains("F#129", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArcLeadin_UsesVariable127()
|
||||
{
|
||||
var config = DefaultConfig();
|
||||
config.KerfCompensation = KerfMode.PreApplied;
|
||||
var codes = new List<ICode>
|
||||
{
|
||||
new RapidMove(10.0, 20.0),
|
||||
new ArcMove(new Vector(12.0, 20.0), new Vector(11.0, 20.0), RotationType.CCW) { Layer = LayerType.Leadin }
|
||||
};
|
||||
var ctx = SimpleContext(codes);
|
||||
var output = WriteFeature(config, ctx);
|
||||
|
||||
Assert.Contains("F#127", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArcFeedrate_VariablesMode_UsesRadiusRangeVariable()
|
||||
{
|
||||
var config = DefaultConfig();
|
||||
config.KerfCompensation = KerfMode.PreApplied;
|
||||
config.ArcFeedrate = ArcFeedrateMode.Variables;
|
||||
// Arc with radius 0.5 (center at 10.5, 20; start at 10, 20) → R=0.5 → #124 (R ≤ 0.750)
|
||||
var codes = new List<ICode>
|
||||
{
|
||||
new RapidMove(10.0, 20.0),
|
||||
new ArcMove(new Vector(11.0, 20.0), new Vector(10.5, 20.0), RotationType.CW) { Layer = LayerType.Cut }
|
||||
};
|
||||
var ctx = SimpleContext(codes);
|
||||
var output = WriteFeature(config, ctx);
|
||||
|
||||
Assert.Contains("F#124", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArcFeedrate_PercentagesMode_UsesInlineExpression()
|
||||
{
|
||||
var config = DefaultConfig();
|
||||
config.KerfCompensation = KerfMode.PreApplied;
|
||||
config.ArcFeedrate = ArcFeedrateMode.Percentages;
|
||||
// Arc with radius 0.1 (≤ 0.125) → 25%
|
||||
var codes = new List<ICode>
|
||||
{
|
||||
new RapidMove(10.0, 20.0),
|
||||
new ArcMove(new Vector(10.2, 20.0), new Vector(10.1, 20.0), RotationType.CW) { Layer = LayerType.Cut }
|
||||
};
|
||||
var ctx = SimpleContext(codes);
|
||||
var output = WriteFeature(config, ctx);
|
||||
|
||||
Assert.Contains("F[#148*0.25]", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArcFeedrate_LargeRadius_UsesProcessFeedrate()
|
||||
{
|
||||
var config = DefaultConfig();
|
||||
config.KerfCompensation = KerfMode.PreApplied;
|
||||
config.ArcFeedrate = ArcFeedrateMode.Variables;
|
||||
// Arc with radius 10 (> 4.500) → falls through to #148
|
||||
var codes = new List<ICode>
|
||||
{
|
||||
new RapidMove(0.0, 0.0),
|
||||
new ArcMove(new Vector(20.0, 0.0), new Vector(10.0, 0.0), RotationType.CCW) { Layer = LayerType.Cut }
|
||||
};
|
||||
var ctx = SimpleContext(codes);
|
||||
var output = WriteFeature(config, ctx);
|
||||
|
||||
Assert.Contains("F#148", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArcFeedrate_NoneMode_UsesProcessFeedrateForNonCircle()
|
||||
{
|
||||
var config = DefaultConfig();
|
||||
config.KerfCompensation = KerfMode.PreApplied;
|
||||
config.ArcFeedrate = ArcFeedrateMode.None;
|
||||
// Small radius arc but mode is None → process feedrate
|
||||
var codes = new List<ICode>
|
||||
{
|
||||
new RapidMove(10.0, 20.0),
|
||||
new ArcMove(new Vector(10.2, 20.0), new Vector(10.1, 20.0), RotationType.CW) { Layer = LayerType.Cut }
|
||||
};
|
||||
var ctx = SimpleContext(codes);
|
||||
var output = WriteFeature(config, ctx);
|
||||
|
||||
Assert.Contains("F#148", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void G89_PAdjacentToFilename()
|
||||
{
|
||||
var config = DefaultConfig();
|
||||
var ctx = SimpleContext();
|
||||
ctx.LibraryFile = "MS135N2.lib";
|
||||
var output = WriteFeature(config, ctx);
|
||||
|
||||
// P must be directly adjacent to filename, no space
|
||||
Assert.Contains("G89 PMS135N2.lib", output);
|
||||
Assert.DoesNotContain("G89 P MS135N2.lib", output);
|
||||
}
|
||||
|
||||
private static int CountOccurrences(string text, string pattern)
|
||||
{
|
||||
var count = 0;
|
||||
|
||||
@@ -43,6 +43,74 @@ public class CincinnatiPostProcessorTests
|
||||
Assert.Contains("M99", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Post_EmitsLeadOutVariable()
|
||||
{
|
||||
var nest = CreateTestNest();
|
||||
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());
|
||||
Assert.Contains("#129=", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Post_WithArcFeedrateVariables_EmitsRangeVariables()
|
||||
{
|
||||
var nest = CreateTestNest();
|
||||
var config = new CincinnatiPostConfig
|
||||
{
|
||||
PostedAccuracy = 4,
|
||||
ArcFeedrate = ArcFeedrateMode.Variables
|
||||
};
|
||||
var post = new CincinnatiPostProcessor(config);
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
post.Post(nest, ms);
|
||||
|
||||
var output = Encoding.UTF8.GetString(ms.ToArray());
|
||||
Assert.Contains("#123=", output);
|
||||
Assert.Contains("#124=", output);
|
||||
Assert.Contains("#125=", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Post_WithArcFeedrateNone_OmitsRangeVariables()
|
||||
{
|
||||
var nest = CreateTestNest();
|
||||
var config = new CincinnatiPostConfig
|
||||
{
|
||||
PostedAccuracy = 4,
|
||||
ArcFeedrate = ArcFeedrateMode.None
|
||||
};
|
||||
var post = new CincinnatiPostProcessor(config);
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
post.Post(nest, ms);
|
||||
|
||||
var output = Encoding.UTF8.GetString(ms.ToArray());
|
||||
Assert.DoesNotContain("#123=", output);
|
||||
Assert.DoesNotContain("#124=", output);
|
||||
Assert.DoesNotContain("#125=", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Post_EmitsG90InPreamble()
|
||||
{
|
||||
var nest = CreateTestNest();
|
||||
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());
|
||||
Assert.Contains("G20 G90", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Post_ImplementsIPostProcessor()
|
||||
{
|
||||
|
||||
@@ -24,9 +24,9 @@ public class CincinnatiPreambleWriterTests
|
||||
var output = sb.ToString();
|
||||
Assert.Contains("( NEST TestNest )", output);
|
||||
Assert.Contains("( CONFIGURATION - CL940 )", output);
|
||||
Assert.Contains("G20", output);
|
||||
Assert.Contains("G20 G90", output);
|
||||
Assert.Contains("M42", output);
|
||||
Assert.Contains("G89 P MS135N2PANEL.lib", output);
|
||||
Assert.Contains("G89 PMS135N2PANEL.lib", output);
|
||||
Assert.Contains("M98 P100 (Variable Declaration)", output);
|
||||
Assert.Contains("GOTO1 (GOTO SHEET NUMBER)", output);
|
||||
Assert.Contains("N1 M98 P101 (SHEET 1)", output);
|
||||
@@ -44,7 +44,72 @@ public class CincinnatiPreambleWriterTests
|
||||
|
||||
writer.WriteMainProgram(sw, "Test", "", 1, "");
|
||||
|
||||
Assert.Contains("G21", sb.ToString());
|
||||
Assert.Contains("G21 G90", sb.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteMainProgram_EmitsG90WithUnits()
|
||||
{
|
||||
var config = new CincinnatiPostConfig { PostedUnits = Units.Inches };
|
||||
var sb = new StringBuilder();
|
||||
using var sw = new StringWriter(sb);
|
||||
var writer = new CincinnatiPreambleWriter(config);
|
||||
|
||||
writer.WriteMainProgram(sw, "Test", "", 1, "");
|
||||
|
||||
Assert.Contains("G20 G90", sb.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteMainProgram_EmitsG121_WhenSmartRapidsEnabled()
|
||||
{
|
||||
var config = new CincinnatiPostConfig { UseSmartRapids = true };
|
||||
var sb = new StringBuilder();
|
||||
using var sw = new StringWriter(sb);
|
||||
var writer = new CincinnatiPreambleWriter(config);
|
||||
|
||||
writer.WriteMainProgram(sw, "Test", "", 1, "");
|
||||
|
||||
Assert.Contains("G121 (SMART RAPIDS)", sb.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteMainProgram_OmitsG121_WhenSmartRapidsDisabled()
|
||||
{
|
||||
var config = new CincinnatiPostConfig { UseSmartRapids = false };
|
||||
var sb = new StringBuilder();
|
||||
using var sw = new StringWriter(sb);
|
||||
var writer = new CincinnatiPreambleWriter(config);
|
||||
|
||||
writer.WriteMainProgram(sw, "Test", "", 1, "");
|
||||
|
||||
Assert.DoesNotContain("G121", sb.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteMainProgram_EmitsM50_WhenStartAndEnd()
|
||||
{
|
||||
var config = new CincinnatiPostConfig { PalletExchange = PalletMode.StartAndEnd };
|
||||
var sb = new StringBuilder();
|
||||
using var sw = new StringWriter(sb);
|
||||
var writer = new CincinnatiPreambleWriter(config);
|
||||
|
||||
writer.WriteMainProgram(sw, "Test", "", 1, "");
|
||||
|
||||
Assert.Contains("M50", sb.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteMainProgram_OmitsM50_WhenEndOfSheet()
|
||||
{
|
||||
var config = new CincinnatiPostConfig { PalletExchange = PalletMode.EndOfSheet };
|
||||
var sb = new StringBuilder();
|
||||
using var sw = new StringWriter(sb);
|
||||
var writer = new CincinnatiPreambleWriter(config);
|
||||
|
||||
writer.WriteMainProgram(sw, "Test", "", 1, "");
|
||||
|
||||
Assert.DoesNotContain("M50", sb.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -32,7 +32,7 @@ public class CincinnatiSheetWriterTests
|
||||
Assert.Contains("#110=", output);
|
||||
Assert.Contains("#111=", output);
|
||||
Assert.Contains("G92 X#5021 Y#5022", output);
|
||||
Assert.Contains("G89 P MS135N2PANEL.lib", output);
|
||||
Assert.Contains("G89 PMS135N2PANEL.lib", output);
|
||||
Assert.Contains("M99", output);
|
||||
}
|
||||
|
||||
@@ -137,9 +137,110 @@ public class CincinnatiSheetWriterTests
|
||||
Assert.True(g85Idx < g84Idx, "G85 (etch) should come before G84 (cut)");
|
||||
|
||||
// Etch uses etch library
|
||||
Assert.Contains("G89 P EtchN2.lib", output);
|
||||
Assert.Contains("G89 PEtchN2.lib", output);
|
||||
// Cut uses cut library
|
||||
Assert.Contains("G89 P MS250O2.lib", output);
|
||||
Assert.Contains("G89 PMS250O2.lib", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteSheet_StartAndEnd_NoM50OnNonLastSheet()
|
||||
{
|
||||
var config = new CincinnatiPostConfig
|
||||
{
|
||||
PalletExchange = PalletMode.StartAndEnd,
|
||||
PostedAccuracy = 4
|
||||
};
|
||||
var plate = new Plate(48.0, 96.0);
|
||||
plate.Parts.Add(new Part(new Drawing("TestPart", CreateSimpleProgram())));
|
||||
|
||||
var sb = new StringBuilder();
|
||||
using var sw = new StringWriter(sb);
|
||||
var sheetWriter = new CincinnatiSheetWriter(config, new ProgramVariableManager());
|
||||
|
||||
sheetWriter.Write(sw, plate, "TestNest", 1, 101, "", "", isLastSheet: false);
|
||||
|
||||
var output = sb.ToString();
|
||||
Assert.DoesNotContain("M50", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteSheet_StartAndEnd_M50OnLastSheet()
|
||||
{
|
||||
var config = new CincinnatiPostConfig
|
||||
{
|
||||
PalletExchange = PalletMode.StartAndEnd,
|
||||
PostedAccuracy = 4
|
||||
};
|
||||
var plate = new Plate(48.0, 96.0);
|
||||
plate.Parts.Add(new Part(new Drawing("TestPart", CreateSimpleProgram())));
|
||||
|
||||
var sb = new StringBuilder();
|
||||
using var sw = new StringWriter(sb);
|
||||
var sheetWriter = new CincinnatiSheetWriter(config, new ProgramVariableManager());
|
||||
|
||||
sheetWriter.Write(sw, plate, "TestNest", 1, 101, "", "", isLastSheet: true);
|
||||
|
||||
var output = sb.ToString();
|
||||
Assert.Contains("M50", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteSheet_EndOfSheet_AlwaysEmitsM50()
|
||||
{
|
||||
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, "", "", isLastSheet: false);
|
||||
|
||||
var output = sb.ToString();
|
||||
Assert.Contains("M50", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeArcLength_FullCircle_Returns2PiR()
|
||||
{
|
||||
var start = new Vector(10.0, 20.0);
|
||||
var arc = new ArcMove(new Vector(10.0, 20.0), new Vector(15.0, 20.0), RotationType.CW);
|
||||
var length = FeatureUtils.ComputeArcLength(start, arc);
|
||||
|
||||
// Radius = 5, full circle = 2 * PI * 5 ≈ 31.416
|
||||
Assert.Equal(2.0 * System.Math.PI * 5.0, length, 4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeArcLength_Semicircle_ReturnsPiR()
|
||||
{
|
||||
// Semicircle from (0,0) to (10,0) with center at (5,0), CCW → goes through (5,5)
|
||||
var start = new Vector(0.0, 0.0);
|
||||
var arc = new ArcMove(new Vector(10.0, 0.0), new Vector(5.0, 0.0), RotationType.CCW);
|
||||
var length = FeatureUtils.ComputeArcLength(start, arc);
|
||||
|
||||
// Radius = 5, semicircle = PI * 5 ≈ 15.708
|
||||
Assert.Equal(System.Math.PI * 5.0, length, 4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeCutDistance_WithArcs_UsesArcLengthNotChord()
|
||||
{
|
||||
// Full circle: chord = 0 but arc length = 2πr
|
||||
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 distance = FeatureUtils.ComputeCutDistance(codes);
|
||||
|
||||
// Full circle with R=5 → 2πr ≈ 31.416
|
||||
Assert.True(distance > 30.0, $"Expected arc length > 30 but got {distance}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user