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:
2026-03-30 09:33:50 -04:00
parent 722f758e94
commit 3d4204db7b
10 changed files with 515 additions and 22 deletions
@@ -70,7 +70,7 @@ public sealed class CincinnatiFeatureWriter
{ {
var speedClass = _speedClassifier.Classify(ctx.CutDistance, ctx.SheetDiagonal); var speedClass = _speedClassifier.Classify(ctx.CutDistance, ctx.SheetDiagonal);
var cutDist = _speedClassifier.FormatCutDist(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 else
{ {
@@ -108,7 +108,7 @@ public sealed class CincinnatiFeatureWriter
sb.Append($"G1 X{_fmt.FormatCoord(linear.EndPoint.X)} Y{_fmt.FormatCoord(linear.EndPoint.Y)}"); sb.Append($"G1 X{_fmt.FormatCoord(linear.EndPoint.X)} Y{_fmt.FormatCoord(linear.EndPoint.Y)}");
// Feedrate — etch always uses process feedrate // 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) if (feedVar != lastFeedVar)
{ {
sb.Append($" F{feedVar}"); sb.Append($" F{feedVar}");
@@ -138,11 +138,11 @@ public sealed class CincinnatiFeatureWriter
var j = arc.CenterPoint.Y - currentPos.Y; var j = arc.CenterPoint.Y - currentPos.Y;
sb.Append($" I{_fmt.FormatCoord(i)} J{_fmt.FormatCoord(j)}"); 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 isFullCircle = IsFullCircle(currentPos, arc.EndPoint);
var feedVar = ctx.IsEtch ? "#148" var feedVar = ctx.IsEtch ? "#148"
: isFullCircle ? "[#148*#128]" : GetArcFeedrate(arc.Layer, radius, isFullCircle);
: GetFeedVariable(arc.Layer);
if (feedVar != lastFeedVar) if (feedVar != lastFeedVar)
{ {
sb.Append($" F{feedVar}"); 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 return layer switch
{ {
LayerType.Leadin => "#126", LayerType.Leadin => "#126",
LayerType.Cut => "#148", LayerType.Leadout => "#129",
_ => "#148" _ => "#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) private static bool IsFullCircle(Vector start, Vector end)
{ {
return Tolerance.IsEqualTo(start.X, end.X) && Tolerance.IsEqualTo(start.Y, end.Y); return Tolerance.IsEqualTo(start.X, end.X) && Tolerance.IsEqualTo(start.Y, end.Y);
@@ -269,12 +269,35 @@ namespace OpenNest.Posts.Cincinnati
/// </summary> /// </summary>
public double LeadInArcLine2FeedratePercent { get; set; } = 0.5; 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> /// <summary>
/// Gets or sets the feedrate multiplier for circular cuts. /// Gets or sets the feedrate multiplier for circular cuts.
/// Default: 0.8 (80%) /// Default: 0.8 (80%)
/// </summary> /// </summary>
public double CircleFeedrateMultiplier { get; set; } = 0.8; 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> /// <summary>
/// Gets or sets the variable number for sheet width. /// Gets or sets the variable number for sheet width.
/// Default: 110 /// Default: 110
@@ -301,4 +324,34 @@ namespace OpenNest.Posts.Cincinnati
public string Gas { get; set; } = ""; public string Gas { get; set; } = "";
public string Library { 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 sheetIndex = i + 1;
var subNumber = Config.SheetSubprogramStart + i; var subNumber = Config.SheetSubprogramStart + i;
var cutLibrary = resolver.ResolveCutLibrary(plate.Material?.Name ?? "", plate.Thickness, gas); 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, sheetWriter.Write(writer, plate, nest.Name ?? "NEST", sheetIndex, subNumber,
cutLibrary, etchLibrary, partSubprograms); cutLibrary, etchLibrary, partSubprograms, isLastSheet);
} }
// Part sub-programs (if enabled) // Part sub-programs (if enabled)
@@ -148,6 +149,18 @@ namespace OpenNest.Posts.Cincinnati
vars.GetOrCreate("LeadInFeedrate", 126, $"[#148*{Config.LeadInFeedratePercent}]"); vars.GetOrCreate("LeadInFeedrate", 126, $"[#148*{Config.LeadInFeedratePercent}]");
vars.GetOrCreate("LeadInArcLine2Feedrate", 127, $"[#148*{Config.LeadInArcLine2FeedratePercent}]"); vars.GetOrCreate("LeadInArcLine2Feedrate", 127, $"[#148*{Config.LeadInArcLine2FeedratePercent}]");
vars.GetOrCreate("CircleFeedrate", 128, Config.CircleFeedrateMultiplier.ToString("0.#")); 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; return vars;
} }
} }
@@ -37,15 +37,21 @@ public sealed class CincinnatiPreambleWriter
w.WriteLine(CoordinateFormatter.Comment("MAIN PROGRAM")); 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"); w.WriteLine("M42");
if (_config.ProcessParameterMode == G89Mode.LibraryFile && !string.IsNullOrEmpty(initialLibrary)) 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)"); w.WriteLine($"M98 P{_config.VariableDeclarationSubprogram} (Variable Declaration)");
if (_config.PalletExchange == PalletMode.StartAndEnd)
w.WriteLine("M50");
w.WriteLine("GOTO1 (GOTO SHEET NUMBER)"); w.WriteLine("GOTO1 (GOTO SHEET NUMBER)");
for (var i = 1; i <= sheetCount; i++) for (var i = 1; i <= sheetCount; i++)
@@ -37,7 +37,8 @@ public sealed class CincinnatiSheetWriter
/// </param> /// </param>
public void Write(TextWriter w, Plate plate, string nestName, int sheetIndex, int subNumber, public void Write(TextWriter w, Plate plate, string nestName, int sheetIndex, int subNumber,
string cutLibrary, string etchLibrary, string cutLibrary, string etchLibrary,
Dictionary<(int, long), int> partSubprograms = null) Dictionary<(int, long), int> partSubprograms = null,
bool isLastSheet = false)
{ {
if (plate.Parts.Count == 0) if (plate.Parts.Count == 0)
return; return;
@@ -64,12 +65,12 @@ public sealed class CincinnatiSheetWriter
w.WriteLine("N10000"); w.WriteLine("N10000");
w.WriteLine("G92 X#5021 Y#5022"); w.WriteLine("G92 X#5021 Y#5022");
if (!string.IsNullOrEmpty(cutLibrary)) if (!string.IsNullOrEmpty(cutLibrary))
w.WriteLine($"G89 P {cutLibrary}"); w.WriteLine($"G89 P{cutLibrary}");
w.WriteLine($"M98 P{varDeclSub} (Variable Declaration)"); w.WriteLine($"M98 P{varDeclSub} (Variable Declaration)");
w.WriteLine("G90"); w.WriteLine("G90");
w.WriteLine("M47"); w.WriteLine("M47");
if (!string.IsNullOrEmpty(cutLibrary)) if (!string.IsNullOrEmpty(cutLibrary))
w.WriteLine($"G89 P {cutLibrary}"); w.WriteLine($"G89 P{cutLibrary}");
w.WriteLine("GOTO1( Goto Feature )"); w.WriteLine("GOTO1( Goto Feature )");
// 3. Order parts: non-cutoff sorted by Bottom then Left, cutoffs last // 3. Order parts: non-cutoff sorted by Bottom then Left, cutoffs last
@@ -94,7 +95,9 @@ public sealed class CincinnatiSheetWriter
// 5. Footer // 5. Footer
w.WriteLine("M42"); w.WriteLine("M42");
w.WriteLine("G0 X0 Y0"); 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($"N{sheetIndex + 1} M50");
w.WriteLine($"M99 (END OF {nestName}.{sheetIndex:D3})"); w.WriteLine($"M99 (END OF {nestName}.{sheetIndex:D3})");
} }
+39 -1
View File
@@ -1,6 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using OpenNest.CNC; using OpenNest.CNC;
using OpenNest.Geometry; using OpenNest.Geometry;
using OpenNest.Math;
namespace OpenNest.Posts.Cincinnati; namespace OpenNest.Posts.Cincinnati;
@@ -105,11 +106,48 @@ public static class FeatureUtils
} }
else if (code is ArcMove arc) else if (code is ArcMove arc)
{ {
distance += currentPos.DistanceTo(arc.EndPoint); distance += ComputeArcLength(currentPos, arc);
currentPos = arc.EndPoint; currentPos = arc.EndPoint;
} }
} }
return distance; 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; ctx.SheetDiagonal = 30.0;
var output = WriteFeature(config, ctx); var output = WriteFeature(config, ctx);
Assert.Contains("G89 P MILD10", output); Assert.Contains("G89 PMILD10", output);
} }
[Fact] [Fact]
@@ -470,6 +470,123 @@ public class CincinnatiFeatureWriterTests
Assert.True(m131Idx < m47Idx, "M131 should come before M47"); 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) private static int CountOccurrences(string text, string pattern)
{ {
var count = 0; var count = 0;
@@ -43,6 +43,74 @@ public class CincinnatiPostProcessorTests
Assert.Contains("M99", output); 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] [Fact]
public void Post_ImplementsIPostProcessor() public void Post_ImplementsIPostProcessor()
{ {
@@ -24,9 +24,9 @@ public class CincinnatiPreambleWriterTests
var output = sb.ToString(); var output = sb.ToString();
Assert.Contains("( NEST TestNest )", output); Assert.Contains("( NEST TestNest )", output);
Assert.Contains("( CONFIGURATION - CL940 )", output); Assert.Contains("( CONFIGURATION - CL940 )", output);
Assert.Contains("G20", output); Assert.Contains("G20 G90", output);
Assert.Contains("M42", 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("M98 P100 (Variable Declaration)", output);
Assert.Contains("GOTO1 (GOTO SHEET NUMBER)", output); Assert.Contains("GOTO1 (GOTO SHEET NUMBER)", output);
Assert.Contains("N1 M98 P101 (SHEET 1)", output); Assert.Contains("N1 M98 P101 (SHEET 1)", output);
@@ -44,7 +44,72 @@ public class CincinnatiPreambleWriterTests
writer.WriteMainProgram(sw, "Test", "", 1, ""); 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] [Fact]
@@ -32,7 +32,7 @@ public class CincinnatiSheetWriterTests
Assert.Contains("#110=", output); Assert.Contains("#110=", output);
Assert.Contains("#111=", output); Assert.Contains("#111=", output);
Assert.Contains("G92 X#5021 Y#5022", output); Assert.Contains("G92 X#5021 Y#5022", output);
Assert.Contains("G89 P MS135N2PANEL.lib", output); Assert.Contains("G89 PMS135N2PANEL.lib", output);
Assert.Contains("M99", output); Assert.Contains("M99", output);
} }
@@ -137,9 +137,110 @@ public class CincinnatiSheetWriterTests
Assert.True(g85Idx < g84Idx, "G85 (etch) should come before G84 (cut)"); Assert.True(g85Idx < g84Idx, "G85 (etch) should come before G84 (cut)");
// Etch uses etch library // Etch uses etch library
Assert.Contains("G89 P EtchN2.lib", output); Assert.Contains("G89 PEtchN2.lib", output);
// Cut uses cut library // 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] [Fact]