diff --git a/OpenNest.Posts.Cincinnati/CincinnatiFeatureWriter.cs b/OpenNest.Posts.Cincinnati/CincinnatiFeatureWriter.cs
index 1602059..ca5bbde 100644
--- a/OpenNest.Posts.Cincinnati/CincinnatiFeatureWriter.cs
+++ b/OpenNest.Posts.Cincinnati/CincinnatiFeatureWriter.cs
@@ -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);
diff --git a/OpenNest.Posts.Cincinnati/CincinnatiPostConfig.cs b/OpenNest.Posts.Cincinnati/CincinnatiPostConfig.cs
index 34092e6..fc59426 100644
--- a/OpenNest.Posts.Cincinnati/CincinnatiPostConfig.cs
+++ b/OpenNest.Posts.Cincinnati/CincinnatiPostConfig.cs
@@ -269,12 +269,35 @@ namespace OpenNest.Posts.Cincinnati
///
public double LeadInArcLine2FeedratePercent { get; set; } = 0.5;
+ ///
+ /// Gets or sets the feedrate percentage for lead-out moves.
+ /// Default: 0.5 (50%)
+ ///
+ public double LeadOutFeedratePercent { get; set; } = 0.5;
+
///
/// Gets or sets the feedrate multiplier for circular cuts.
/// Default: 0.8 (80%)
///
public double CircleFeedrateMultiplier { get; set; } = 0.8;
+ ///
+ /// Gets or sets the arc feedrate calculation mode.
+ /// Default: ArcFeedrateMode.None
+ ///
+ public ArcFeedrateMode ArcFeedrate { get; set; } = ArcFeedrateMode.None;
+
+ ///
+ /// Gets or sets the radius-based arc feedrate ranges.
+ /// Ranges are matched from smallest MaxRadius to largest.
+ ///
+ public List 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 }
+ };
+
///
/// 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; } = "";
}
+
+ ///
+ /// Specifies how arc feedrates are calculated based on radius.
+ ///
+ public enum ArcFeedrateMode
+ {
+ /// No radius-based arc feedrate adjustment (only full circles use multiplier).
+ None,
+
+ /// Inline percentage expressions: F [#148*pct] based on radius range.
+ Percentages,
+
+ /// Radius-range-based variables: F #varNum based on radius range.
+ Variables
+ }
+
+ ///
+ /// Defines a radius range and its associated feedrate for arc moves.
+ ///
+ public class ArcFeedrateRange
+ {
+ /// Maximum radius for this range (inclusive).
+ public double MaxRadius { get; set; }
+
+ /// Feedrate as a fraction of process feedrate (e.g. 0.25 = 25%).
+ public double FeedratePercent { get; set; }
+
+ /// Variable number for Variables mode (e.g. 123).
+ public int VariableNumber { get; set; }
+ }
}
diff --git a/OpenNest.Posts.Cincinnati/CincinnatiPostProcessor.cs b/OpenNest.Posts.Cincinnati/CincinnatiPostProcessor.cs
index 4f764ff..c822989 100644
--- a/OpenNest.Posts.Cincinnati/CincinnatiPostProcessor.cs
+++ b/OpenNest.Posts.Cincinnati/CincinnatiPostProcessor.cs
@@ -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;
}
}
diff --git a/OpenNest.Posts.Cincinnati/CincinnatiPreambleWriter.cs b/OpenNest.Posts.Cincinnati/CincinnatiPreambleWriter.cs
index f119b42..85c830c 100644
--- a/OpenNest.Posts.Cincinnati/CincinnatiPreambleWriter.cs
+++ b/OpenNest.Posts.Cincinnati/CincinnatiPreambleWriter.cs
@@ -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++)
diff --git a/OpenNest.Posts.Cincinnati/CincinnatiSheetWriter.cs b/OpenNest.Posts.Cincinnati/CincinnatiSheetWriter.cs
index ad1fb13..084f876 100644
--- a/OpenNest.Posts.Cincinnati/CincinnatiSheetWriter.cs
+++ b/OpenNest.Posts.Cincinnati/CincinnatiSheetWriter.cs
@@ -37,7 +37,8 @@ public sealed class CincinnatiSheetWriter
///
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})");
}
diff --git a/OpenNest.Posts.Cincinnati/FeatureUtils.cs b/OpenNest.Posts.Cincinnati/FeatureUtils.cs
index 8fb3759..20fc501 100644
--- a/OpenNest.Posts.Cincinnati/FeatureUtils.cs
+++ b/OpenNest.Posts.Cincinnati/FeatureUtils.cs
@@ -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;
}
+
+ ///
+ /// Computes the arc length from the current position through an arc move.
+ /// Uses radius * sweep angle instead of chord length.
+ ///
+ 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;
+ }
}
diff --git a/OpenNest.Tests/Cincinnati/CincinnatiFeatureWriterTests.cs b/OpenNest.Tests/Cincinnati/CincinnatiFeatureWriterTests.cs
index bf88a41..860bc35 100644
--- a/OpenNest.Tests/Cincinnati/CincinnatiFeatureWriterTests.cs
+++ b/OpenNest.Tests/Cincinnati/CincinnatiFeatureWriterTests.cs
@@ -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
+ {
+ 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
+ {
+ 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
+ {
+ 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
+ {
+ 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
+ {
+ 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
+ {
+ 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;
diff --git a/OpenNest.Tests/Cincinnati/CincinnatiPostProcessorTests.cs b/OpenNest.Tests/Cincinnati/CincinnatiPostProcessorTests.cs
index d99c237..1e004c8 100644
--- a/OpenNest.Tests/Cincinnati/CincinnatiPostProcessorTests.cs
+++ b/OpenNest.Tests/Cincinnati/CincinnatiPostProcessorTests.cs
@@ -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()
{
diff --git a/OpenNest.Tests/Cincinnati/CincinnatiPreambleWriterTests.cs b/OpenNest.Tests/Cincinnati/CincinnatiPreambleWriterTests.cs
index b663880..52eaa7a 100644
--- a/OpenNest.Tests/Cincinnati/CincinnatiPreambleWriterTests.cs
+++ b/OpenNest.Tests/Cincinnati/CincinnatiPreambleWriterTests.cs
@@ -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]
diff --git a/OpenNest.Tests/Cincinnati/CincinnatiSheetWriterTests.cs b/OpenNest.Tests/Cincinnati/CincinnatiSheetWriterTests.cs
index 7d94cc4..3fdc8b0 100644
--- a/OpenNest.Tests/Cincinnati/CincinnatiSheetWriterTests.cs
+++ b/OpenNest.Tests/Cincinnati/CincinnatiSheetWriterTests.cs
@@ -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
+ {
+ 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]