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:
@@ -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,7 +37,10 @@ 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");
|
||||||
|
|
||||||
@@ -46,6 +49,9 @@ public sealed class CincinnatiPreambleWriter
|
|||||||
|
|
||||||
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;
|
||||||
@@ -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})");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,7 +24,7 @@ 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 PMS135N2PANEL.lib", output);
|
Assert.Contains("G89 PMS135N2PANEL.lib", output);
|
||||||
Assert.Contains("M98 P100 (Variable Declaration)", output);
|
Assert.Contains("M98 P100 (Variable Declaration)", 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]
|
||||||
|
|||||||
@@ -142,6 +142,107 @@ public class CincinnatiSheetWriterTests
|
|||||||
Assert.Contains("G89 PMS250O2.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]
|
||||||
public void IsFeatureEtch_ReturnsTrueForScribeLayer()
|
public void IsFeatureEtch_ReturnsTrueForScribeLayer()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user