diff --git a/OpenNest.Posts.Cincinnati/CincinnatiFeatureWriter.cs b/OpenNest.Posts.Cincinnati/CincinnatiFeatureWriter.cs
new file mode 100644
index 0000000..6435afe
--- /dev/null
+++ b/OpenNest.Posts.Cincinnati/CincinnatiFeatureWriter.cs
@@ -0,0 +1,230 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using OpenNest.CNC;
+using OpenNest.Geometry;
+using OpenNest.Math;
+
+namespace OpenNest.Posts.Cincinnati;
+
+///
+/// Data class carrying all context needed to emit one Cincinnati-format G-code feature block.
+///
+public sealed class FeatureContext
+{
+ public List Codes { get; set; } = new();
+ public int FeatureNumber { get; set; }
+ public string PartName { get; set; } = "";
+ public bool IsFirstFeatureOfPart { get; set; }
+ public bool IsLastFeatureOnSheet { get; set; }
+ public bool IsSafetyHeadraise { get; set; }
+ public bool IsExteriorFeature { get; set; }
+ public string LibraryFile { get; set; } = "";
+ public double CutDistance { get; set; }
+ public double SheetDiagonal { get; set; }
+}
+
+///
+/// Emits one Cincinnati-format G-code feature block (one contour) to a TextWriter.
+/// Handles rapid positioning, pierce, kerf compensation, anti-dive, feedrate modal
+/// suppression, arc I/J conversion (absolute to incremental), and M47 head raise.
+///
+public sealed class CincinnatiFeatureWriter
+{
+ private readonly CincinnatiPostConfig _config;
+ private readonly CoordinateFormatter _fmt;
+ private readonly SpeedClassifier _speedClassifier;
+
+ public CincinnatiFeatureWriter(CincinnatiPostConfig config)
+ {
+ _config = config;
+ _fmt = new CoordinateFormatter(config.PostedAccuracy);
+ _speedClassifier = new SpeedClassifier();
+ }
+
+ ///
+ /// Writes a complete feature block for the given context.
+ ///
+ public void Write(TextWriter writer, FeatureContext ctx)
+ {
+ var currentPos = Vector.Zero;
+ var lastFeedVar = "";
+ var kerfEmitted = false;
+
+ // Find the pierce point from the first rapid move
+ var piercePoint = FindPiercePoint(ctx.Codes);
+
+ // 1. Rapid to pierce point (with line number if configured)
+ WriteRapidToPierce(writer, ctx.FeatureNumber, piercePoint);
+
+ // 2. Part name comment on first feature of each part
+ if (ctx.IsFirstFeatureOfPart && !string.IsNullOrEmpty(ctx.PartName))
+ writer.WriteLine(CoordinateFormatter.Comment($"PART: {ctx.PartName}"));
+
+ // 3. G89 process params (if RepeatG89BeforeEachFeature)
+ if (_config.RepeatG89BeforeEachFeature && _config.ProcessParameterMode == G89Mode.LibraryFile)
+ {
+ var lib = !string.IsNullOrEmpty(ctx.LibraryFile) ? ctx.LibraryFile : _config.DefaultLibraryFile;
+ var speedClass = _speedClassifier.Classify(ctx.CutDistance, ctx.SheetDiagonal);
+ var cutDist = _speedClassifier.FormatCutDist(ctx.CutDistance, ctx.SheetDiagonal);
+ writer.WriteLine($"G89 P {lib} ({speedClass} {cutDist})");
+ }
+
+ // 4. Pierce and start cut
+ writer.WriteLine("G84");
+
+ // 5. Anti-dive off
+ if (_config.UseAntiDive)
+ writer.WriteLine("M130 (ANTI DIVE OFF)");
+
+ // Update current position to pierce point
+ currentPos = piercePoint;
+
+ // 6. Lead-in + contour moves with kerf comp and feedrate variables
+ foreach (var code in ctx.Codes)
+ {
+ if (code is RapidMove)
+ continue; // skip rapids in contour (already handled above)
+
+ if (code is LinearMove linear)
+ {
+ var sb = new StringBuilder();
+
+ // Kerf compensation on first cutting move
+ if (!kerfEmitted && _config.KerfCompensation == KerfMode.ControllerSide)
+ {
+ sb.Append(_config.DefaultKerfSide == KerfSide.Left ? "G41" : "G42");
+ kerfEmitted = true;
+ }
+
+ sb.Append($"G1X{_fmt.FormatCoord(linear.EndPoint.X)}Y{_fmt.FormatCoord(linear.EndPoint.Y)}");
+
+ // Feedrate
+ var feedVar = GetFeedVariable(linear.Layer);
+ if (feedVar != lastFeedVar)
+ {
+ sb.Append($"F{feedVar}");
+ lastFeedVar = feedVar;
+ }
+
+ writer.WriteLine(sb.ToString());
+ currentPos = linear.EndPoint;
+ }
+ else if (code is ArcMove arc)
+ {
+ var sb = new StringBuilder();
+
+ // Kerf compensation on first cutting move
+ if (!kerfEmitted && _config.KerfCompensation == KerfMode.ControllerSide)
+ {
+ sb.Append(_config.DefaultKerfSide == KerfSide.Left ? "G41" : "G42");
+ kerfEmitted = true;
+ }
+
+ // G2 = CW, G3 = CCW
+ var gCode = arc.Rotation == RotationType.CW ? "G2" : "G3";
+ sb.Append($"{gCode}X{_fmt.FormatCoord(arc.EndPoint.X)}Y{_fmt.FormatCoord(arc.EndPoint.Y)}");
+
+ // Convert absolute center to incremental I/J
+ var i = arc.CenterPoint.X - currentPos.X;
+ var j = arc.CenterPoint.Y - currentPos.Y;
+ sb.Append($"I{_fmt.FormatCoord(i)}J{_fmt.FormatCoord(j)}");
+
+ // Feedrate — full circles use multiplied feedrate
+ var isFullCircle = IsFullCircle(currentPos, arc.EndPoint);
+ var feedVar = isFullCircle ? "[#148*#128]" : GetFeedVariable(arc.Layer);
+ if (feedVar != lastFeedVar)
+ {
+ sb.Append($"F{feedVar}");
+ lastFeedVar = feedVar;
+ }
+
+ writer.WriteLine(sb.ToString());
+ currentPos = arc.EndPoint;
+ }
+ }
+
+ // 7. Cancel kerf compensation
+ if (kerfEmitted)
+ writer.WriteLine("G40");
+
+ // 8. Beam off
+ writer.WriteLine(_config.UseSpeedGas ? "M135" : "M35");
+
+ // 9. Anti-dive on
+ if (_config.UseAntiDive)
+ writer.WriteLine("M131 (ANTI DIVE ON)");
+
+ // 10. Head raise (unless last feature on sheet)
+ if (!ctx.IsLastFeatureOnSheet)
+ WriteM47(writer, ctx);
+ }
+
+ private Vector FindPiercePoint(List codes)
+ {
+ foreach (var code in codes)
+ {
+ if (code is RapidMove rapid)
+ return rapid.EndPoint;
+ }
+
+ // If no rapid move, use the endpoint of the first motion
+ foreach (var code in codes)
+ {
+ if (code is Motion motion)
+ return motion.EndPoint;
+ }
+
+ return Vector.Zero;
+ }
+
+ private void WriteRapidToPierce(TextWriter writer, int featureNumber, Vector piercePoint)
+ {
+ var sb = new StringBuilder();
+
+ if (_config.UseLineNumbers)
+ sb.Append($"N{featureNumber}");
+
+ sb.Append($"G0X{_fmt.FormatCoord(piercePoint.X)}Y{_fmt.FormatCoord(piercePoint.Y)}");
+
+ writer.WriteLine(sb.ToString());
+ }
+
+ private void WriteM47(TextWriter writer, FeatureContext ctx)
+ {
+ if (ctx.IsSafetyHeadraise && _config.SafetyHeadraiseDistance.HasValue)
+ {
+ writer.WriteLine($"M47 P{_config.SafetyHeadraiseDistance.Value}(Safety Headraise)");
+ return;
+ }
+
+ var mode = ctx.IsExteriorFeature ? _config.ExteriorM47 : _config.InteriorM47;
+
+ switch (mode)
+ {
+ case M47Mode.Always:
+ writer.WriteLine("M47");
+ break;
+ case M47Mode.BlockDelete:
+ writer.WriteLine("/M47");
+ break;
+ case M47Mode.None:
+ break;
+ }
+ }
+
+ private static string GetFeedVariable(LayerType layer)
+ {
+ return layer switch
+ {
+ LayerType.Leadin => "#126",
+ LayerType.Cut => "#148",
+ _ => "#148"
+ };
+ }
+
+ private static bool IsFullCircle(Vector start, Vector end)
+ {
+ return Tolerance.IsEqualTo(start.X, end.X) && Tolerance.IsEqualTo(start.Y, end.Y);
+ }
+}
diff --git a/OpenNest.Tests/Cincinnati/CincinnatiFeatureWriterTests.cs b/OpenNest.Tests/Cincinnati/CincinnatiFeatureWriterTests.cs
new file mode 100644
index 0000000..450544c
--- /dev/null
+++ b/OpenNest.Tests/Cincinnati/CincinnatiFeatureWriterTests.cs
@@ -0,0 +1,437 @@
+using OpenNest.CNC;
+using OpenNest.Geometry;
+using OpenNest.Posts.Cincinnati;
+
+namespace OpenNest.Tests.Cincinnati;
+
+public class CincinnatiFeatureWriterTests
+{
+ private static CincinnatiPostConfig DefaultConfig() => new()
+ {
+ UseLineNumbers = true,
+ FeatureLineNumberStart = 1,
+ UseAntiDive = true,
+ KerfCompensation = KerfMode.ControllerSide,
+ DefaultKerfSide = KerfSide.Left,
+ RepeatG89BeforeEachFeature = true,
+ ProcessParameterMode = G89Mode.LibraryFile,
+ DefaultLibraryFile = "MILD10",
+ InteriorM47 = M47Mode.Always,
+ ExteriorM47 = M47Mode.Always,
+ UseSpeedGas = false,
+ PostedAccuracy = 4,
+ SafetyHeadraiseDistance = 2000
+ };
+
+ private static FeatureContext SimpleContext(List? codes = null) => new()
+ {
+ Codes = codes ?? new List
+ {
+ new RapidMove(13.401, 57.4895),
+ new LinearMove(14.0, 57.5) { Layer = LayerType.Leadin },
+ new LinearMove(20.0, 57.5) { Layer = LayerType.Cut }
+ },
+ FeatureNumber = 1,
+ PartName = "BRACKET",
+ IsFirstFeatureOfPart = true,
+ IsLastFeatureOnSheet = false,
+ IsSafetyHeadraise = false,
+ IsExteriorFeature = false,
+ LibraryFile = "MILD10",
+ CutDistance = 18.0,
+ SheetDiagonal = 30.0
+ };
+
+ private static string WriteFeature(CincinnatiPostConfig config, FeatureContext ctx)
+ {
+ var writer = new CincinnatiFeatureWriter(config);
+ using var sw = new StringWriter();
+ writer.Write(sw, ctx);
+ return sw.ToString();
+ }
+
+ [Fact]
+ public void RapidToPiercePoint_WithLineNumber()
+ {
+ var config = DefaultConfig();
+ var ctx = SimpleContext();
+ var output = WriteFeature(config, ctx);
+ var lines = output.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries);
+
+ Assert.StartsWith("N1G0X13.401Y57.4895", lines[0]);
+ }
+
+ [Fact]
+ public void RapidToPiercePoint_WithoutLineNumber()
+ {
+ var config = DefaultConfig();
+ config.UseLineNumbers = false;
+ var ctx = SimpleContext();
+ var output = WriteFeature(config, ctx);
+ var lines = output.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries);
+
+ Assert.StartsWith("G0X13.401Y57.4895", lines[0]);
+ }
+
+ [Fact]
+ public void G84_PierceEmitted()
+ {
+ var config = DefaultConfig();
+ var ctx = SimpleContext();
+ var output = WriteFeature(config, ctx);
+
+ Assert.Contains("G84", output);
+ }
+
+ [Fact]
+ public void AntiDive_M130M131_EmittedWhenEnabled()
+ {
+ var config = DefaultConfig();
+ config.UseAntiDive = true;
+ var ctx = SimpleContext();
+ var output = WriteFeature(config, ctx);
+
+ Assert.Contains("M130 (ANTI DIVE OFF)", output);
+ Assert.Contains("M131 (ANTI DIVE ON)", output);
+ }
+
+ [Fact]
+ public void AntiDive_NotEmittedWhenDisabled()
+ {
+ var config = DefaultConfig();
+ config.UseAntiDive = false;
+ var ctx = SimpleContext();
+ var output = WriteFeature(config, ctx);
+
+ Assert.DoesNotContain("M130", output);
+ Assert.DoesNotContain("M131", output);
+ }
+
+ [Fact]
+ public void KerfCompensation_G41G40_EmittedWhenControllerSide()
+ {
+ var config = DefaultConfig();
+ config.KerfCompensation = KerfMode.ControllerSide;
+ config.DefaultKerfSide = KerfSide.Left;
+ var ctx = SimpleContext();
+ var output = WriteFeature(config, ctx);
+
+ Assert.Contains("G41", output);
+ Assert.Contains("G40", output);
+ }
+
+ [Fact]
+ public void KerfCompensation_G42_EmittedForRightSide()
+ {
+ var config = DefaultConfig();
+ config.KerfCompensation = KerfMode.ControllerSide;
+ config.DefaultKerfSide = KerfSide.Right;
+ var ctx = SimpleContext();
+ var output = WriteFeature(config, ctx);
+
+ Assert.Contains("G42", output);
+ }
+
+ [Fact]
+ public void KerfCompensation_NotEmittedWhenPreApplied()
+ {
+ var config = DefaultConfig();
+ config.KerfCompensation = KerfMode.PreApplied;
+ var ctx = SimpleContext();
+ var output = WriteFeature(config, ctx);
+
+ Assert.DoesNotContain("G41", output);
+ Assert.DoesNotContain("G42", output);
+ Assert.DoesNotContain("G40", output);
+ }
+
+ [Fact]
+ public void M35_BeamOffEmitted()
+ {
+ var config = DefaultConfig();
+ config.UseSpeedGas = false;
+ var ctx = SimpleContext();
+ var output = WriteFeature(config, ctx);
+
+ Assert.Contains("M35", output);
+ }
+
+ [Fact]
+ public void M135_BeamOffEmittedWhenSpeedGas()
+ {
+ var config = DefaultConfig();
+ config.UseSpeedGas = true;
+ var ctx = SimpleContext();
+ var output = WriteFeature(config, ctx);
+
+ Assert.Contains("M135", output);
+ }
+
+ [Fact]
+ public void M47_EmittedWhenNotLastFeature()
+ {
+ var config = DefaultConfig();
+ var ctx = SimpleContext();
+ ctx.IsLastFeatureOnSheet = false;
+ var output = WriteFeature(config, ctx);
+
+ Assert.Contains("M47", output);
+ }
+
+ [Fact]
+ public void M47_OmittedWhenLastFeatureOnSheet()
+ {
+ var config = DefaultConfig();
+ var ctx = SimpleContext();
+ ctx.IsLastFeatureOnSheet = true;
+ var output = WriteFeature(config, ctx);
+
+ // M47 should not appear, but M35 should still be there
+ var lines = output.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries);
+ Assert.DoesNotContain(lines, l => l.Trim() == "M47" || l.Trim() == "/M47");
+ }
+
+ [Fact]
+ public void M47_BlockDeleteMode_EmitsSlashM47()
+ {
+ var config = DefaultConfig();
+ config.InteriorM47 = M47Mode.BlockDelete;
+ var ctx = SimpleContext();
+ ctx.IsExteriorFeature = false;
+ ctx.IsLastFeatureOnSheet = false;
+ var output = WriteFeature(config, ctx);
+
+ Assert.Contains("/M47", output);
+ }
+
+ [Fact]
+ public void M47_NoneMode_NoM47Emitted()
+ {
+ var config = DefaultConfig();
+ config.InteriorM47 = M47Mode.None;
+ var ctx = SimpleContext();
+ ctx.IsExteriorFeature = false;
+ ctx.IsLastFeatureOnSheet = false;
+ var output = WriteFeature(config, ctx);
+
+ var lines = output.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries);
+ Assert.DoesNotContain(lines, l => l.Trim() == "M47" || l.Trim() == "/M47");
+ }
+
+ [Fact]
+ public void ArcIJ_ConvertedFromAbsoluteToIncremental()
+ {
+ var config = DefaultConfig();
+ // Arc starts at rapid endpoint (10, 20), center at (15, 20) absolute
+ // So incremental I = 15 - 10 = 5, J = 20 - 20 = 0
+ var codes = new List
+ {
+ new RapidMove(10.0, 20.0),
+ new ArcMove(
+ endPoint: new Vector(10.0, 20.0),
+ centerPoint: new Vector(15.0, 20.0),
+ rotation: RotationType.CW
+ ) { Layer = LayerType.Cut }
+ };
+
+ var ctx = SimpleContext(codes);
+ var output = WriteFeature(config, ctx);
+
+ // Should contain incremental I=5, J=0
+ Assert.Contains("I5", output);
+ Assert.Contains("J0", output);
+ }
+
+ [Fact]
+ public void ArcMove_G2ForCW_G3ForCCW()
+ {
+ var config = DefaultConfig();
+ var cwCodes = new List
+ {
+ new RapidMove(10.0, 20.0),
+ new ArcMove(new Vector(20.0, 20.0), new Vector(15.0, 20.0), RotationType.CW) { Layer = LayerType.Cut }
+ };
+ var ccwCodes = new List
+ {
+ new RapidMove(10.0, 20.0),
+ new ArcMove(new Vector(20.0, 20.0), new Vector(15.0, 20.0), RotationType.CCW) { Layer = LayerType.Cut }
+ };
+
+ var cwOutput = WriteFeature(config, SimpleContext(cwCodes));
+ var ccwOutput = WriteFeature(config, SimpleContext(ccwCodes));
+
+ Assert.Contains("G2X", cwOutput);
+ Assert.Contains("G3X", ccwOutput);
+ }
+
+ [Fact]
+ public void PartNameComment_EmittedOnFirstFeature()
+ {
+ var config = DefaultConfig();
+ var ctx = SimpleContext();
+ ctx.IsFirstFeatureOfPart = true;
+ ctx.PartName = "FLANGE";
+ var output = WriteFeature(config, ctx);
+
+ Assert.Contains("( PART: FLANGE )", output);
+ }
+
+ [Fact]
+ public void PartNameComment_NotEmittedOnSubsequentFeatures()
+ {
+ var config = DefaultConfig();
+ var ctx = SimpleContext();
+ ctx.IsFirstFeatureOfPart = false;
+ ctx.PartName = "FLANGE";
+ var output = WriteFeature(config, ctx);
+
+ Assert.DoesNotContain("PART:", output);
+ }
+
+ [Fact]
+ public void G89_EmittedWhenRepeatEnabled()
+ {
+ var config = DefaultConfig();
+ config.RepeatG89BeforeEachFeature = true;
+ config.ProcessParameterMode = G89Mode.LibraryFile;
+ config.DefaultLibraryFile = "MILD10";
+ var ctx = SimpleContext();
+ ctx.LibraryFile = "MILD10";
+ ctx.CutDistance = 18.0;
+ ctx.SheetDiagonal = 30.0;
+ var output = WriteFeature(config, ctx);
+
+ Assert.Contains("G89 P MILD10", output);
+ }
+
+ [Fact]
+ public void G89_NotEmittedWhenRepeatDisabled()
+ {
+ var config = DefaultConfig();
+ config.RepeatG89BeforeEachFeature = false;
+ var ctx = SimpleContext();
+ var output = WriteFeature(config, ctx);
+
+ Assert.DoesNotContain("G89", output);
+ }
+
+ [Fact]
+ public void FeedrateModalSuppression_OnlyEmitsOnChange()
+ {
+ var config = DefaultConfig();
+ config.KerfCompensation = KerfMode.PreApplied; // simplify output
+ var codes = new List
+ {
+ new RapidMove(1.0, 1.0),
+ new LinearMove(2.0, 1.0) { Layer = LayerType.Cut },
+ new LinearMove(3.0, 1.0) { Layer = LayerType.Cut },
+ new LinearMove(4.0, 1.0) { Layer = LayerType.Cut }
+ };
+ var ctx = SimpleContext(codes);
+ var output = WriteFeature(config, ctx);
+
+ // F#148 should appear only once (on the first cut move)
+ var count = CountOccurrences(output, "F#148");
+ Assert.Equal(1, count);
+ }
+
+ [Fact]
+ public void LeadinFeedrate_UsesVariable126()
+ {
+ var config = DefaultConfig();
+ config.KerfCompensation = KerfMode.PreApplied; // simplify output
+ var codes = new List
+ {
+ new RapidMove(1.0, 1.0),
+ new LinearMove(2.0, 1.0) { Layer = LayerType.Leadin }
+ };
+ var ctx = SimpleContext(codes);
+ var output = WriteFeature(config, ctx);
+
+ Assert.Contains("F#126", output);
+ }
+
+ [Fact]
+ public void FullCircleArc_UsesMultipliedFeedrate()
+ {
+ var config = DefaultConfig();
+ config.KerfCompensation = KerfMode.PreApplied;
+ // Full circle: start == end
+ var codes = new List
+ {
+ new RapidMove(10.0, 20.0),
+ new ArcMove(new Vector(10.0, 20.0), new Vector(15.0, 20.0), RotationType.CW) { Layer = LayerType.Cut }
+ };
+ var ctx = SimpleContext(codes);
+ var output = WriteFeature(config, ctx);
+
+ Assert.Contains("F[#148*#128]", output);
+ }
+
+ [Fact]
+ public void SafetyHeadraise_EmitsM47WithDistance()
+ {
+ var config = DefaultConfig();
+ config.SafetyHeadraiseDistance = 2000;
+ var ctx = SimpleContext();
+ ctx.IsSafetyHeadraise = true;
+ ctx.IsLastFeatureOnSheet = false;
+ var output = WriteFeature(config, ctx);
+
+ Assert.Contains("M47 P2000(Safety Headraise)", output);
+ }
+
+ [Fact]
+ public void ExteriorM47Mode_UsesExteriorConfig()
+ {
+ var config = DefaultConfig();
+ config.ExteriorM47 = M47Mode.BlockDelete;
+ config.InteriorM47 = M47Mode.Always;
+ var ctx = SimpleContext();
+ ctx.IsExteriorFeature = true;
+ ctx.IsLastFeatureOnSheet = false;
+ var output = WriteFeature(config, ctx);
+
+ Assert.Contains("/M47", output);
+ }
+
+ [Fact]
+ public void OutputSequence_CorrectOrder()
+ {
+ var config = DefaultConfig();
+ var ctx = SimpleContext();
+ var output = WriteFeature(config, ctx);
+ var lines = output.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries);
+
+ // Find indices of key lines
+ var rapidIdx = Array.FindIndex(lines, l => l.Contains("G0X"));
+ var partIdx = Array.FindIndex(lines, l => l.Contains("PART:"));
+ var g89Idx = Array.FindIndex(lines, l => l.Contains("G89"));
+ var g84Idx = Array.FindIndex(lines, l => l.Contains("G84"));
+ var m130Idx = Array.FindIndex(lines, l => l.Contains("M130"));
+ var g40Idx = Array.FindIndex(lines, l => l.Contains("G40"));
+ var m35Idx = Array.FindIndex(lines, l => l.Contains("M35"));
+ var m131Idx = Array.FindIndex(lines, l => l.Contains("M131"));
+ var m47Idx = Array.FindIndex(lines, l => l.Trim() == "M47");
+
+ Assert.True(rapidIdx < partIdx, "Rapid should come before part comment");
+ Assert.True(partIdx < g89Idx, "Part comment should come before G89");
+ Assert.True(g89Idx < g84Idx, "G89 should come before G84");
+ Assert.True(g84Idx < m130Idx, "G84 should come before M130");
+ Assert.True(g40Idx < m35Idx, "G40 should come before M35");
+ Assert.True(m35Idx < m131Idx, "M35 should come before M131");
+ Assert.True(m131Idx < m47Idx, "M131 should come before M47");
+ }
+
+ private static int CountOccurrences(string text, string pattern)
+ {
+ var count = 0;
+ var idx = 0;
+ while ((idx = text.IndexOf(pattern, idx, StringComparison.Ordinal)) != -1)
+ {
+ count++;
+ idx += pattern.Length;
+ }
+ return count;
+ }
+}