diff --git a/OpenNest.Posts.Cincinnati/CincinnatiPartSubprogramWriter.cs b/OpenNest.Posts.Cincinnati/CincinnatiPartSubprogramWriter.cs
index 25a0674..f3cc40a 100644
--- a/OpenNest.Posts.Cincinnati/CincinnatiPartSubprogramWriter.cs
+++ b/OpenNest.Posts.Cincinnati/CincinnatiPartSubprogramWriter.cs
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.IO;
+using System.Text;
using OpenNest.CNC;
using OpenNest.Geometry;
@@ -136,4 +137,61 @@ public sealed class CincinnatiPartSubprogramWriter
return (mapping, entries);
}
+
+ ///
+ /// Scans all parts across all plates and builds a nest-level registry of unique
+ /// hole sub-programs. Deduplicates by comparing sub-program code content.
+ ///
+ internal static (Dictionary modelToPostMapping, List<(int subNum, Program program)> entries)
+ BuildHoleRegistry(IEnumerable plates, int startNumber)
+ {
+ var mapping = new Dictionary();
+ var entries = new List<(int, Program)>();
+ var contentIndex = new Dictionary();
+ var nextSubNum = startNumber;
+
+ foreach (var plate in plates)
+ {
+ foreach (var part in plate.Parts)
+ {
+ if (part.BaseDrawing.IsCutOff) continue;
+ foreach (var code in part.Program.Codes)
+ {
+ if (code is not SubProgramCall call) continue;
+ if (mapping.ContainsKey(call.Id)) continue;
+
+ var canonical = ProgramToCanonical(call.Program);
+ if (contentIndex.TryGetValue(canonical, out var existingNum))
+ {
+ mapping[call.Id] = existingNum;
+ }
+ else
+ {
+ var subNum = nextSubNum++;
+ mapping[call.Id] = subNum;
+ contentIndex[canonical] = subNum;
+ entries.Add((subNum, call.Program));
+ }
+ }
+ }
+ }
+
+ return (mapping, entries);
+ }
+
+ private static string ProgramToCanonical(Program pgm)
+ {
+ var sb = new StringBuilder();
+ sb.Append(pgm.Mode == Mode.Absolute ? "A" : "I");
+ foreach (var code in pgm.Codes)
+ {
+ if (code is LinearMove lm)
+ sb.Append($"L{lm.EndPoint.X:F6},{lm.EndPoint.Y:F6},{(int)lm.Layer}");
+ else if (code is ArcMove am)
+ sb.Append($"A{am.EndPoint.X:F6},{am.EndPoint.Y:F6},{am.CenterPoint.X:F6},{am.CenterPoint.Y:F6},{(int)am.Rotation},{(int)am.Layer}");
+ else if (code is RapidMove rm)
+ sb.Append($"R{rm.EndPoint.X:F6},{rm.EndPoint.Y:F6}");
+ }
+ return sb.ToString();
+ }
}
diff --git a/OpenNest.Posts.Cincinnati/CincinnatiPostProcessor.cs b/OpenNest.Posts.Cincinnati/CincinnatiPostProcessor.cs
index 4d508de..0699b05 100644
--- a/OpenNest.Posts.Cincinnati/CincinnatiPostProcessor.cs
+++ b/OpenNest.Posts.Cincinnati/CincinnatiPostProcessor.cs
@@ -89,9 +89,15 @@ namespace OpenNest.Posts.Cincinnati
if (Config.UsePartSubprograms)
(partSubprograms, subprogramEntries) = CincinnatiPartSubprogramWriter.BuildRegistry(plates, Config.PartSubprogramStart);
+ // 5b. Build hole sub-program registry (SubProgramCalls across all parts)
+ var holeStartNumber = Config.PartSubprogramStart
+ + (subprogramEntries?.Count ?? 0);
+ var (holeMapping, holeEntries) = CincinnatiPartSubprogramWriter.BuildHoleRegistry(plates, holeStartNumber);
+
// 6. Create writers
var preamble = new CincinnatiPreambleWriter(Config);
- var sheetWriter = new CincinnatiSheetWriter(Config, vars);
+ var sheetWriter = new CincinnatiSheetWriter(Config, vars,
+ holeMapping.Count > 0 ? holeMapping : null);
// 7. Build material description from nest
var material = nest.Material;
@@ -135,6 +141,23 @@ namespace OpenNest.Posts.Cincinnati
}
}
+ // Hole sub-programs (SubProgramCall definitions)
+ if (holeEntries.Count > 0)
+ {
+ var holeSubWriter = new CincinnatiPartSubprogramWriter(Config);
+ var sheetDiagonal = firstPlate != null
+ ? System.Math.Sqrt(firstPlate.Size.Width * firstPlate.Size.Width
+ + firstPlate.Size.Length * firstPlate.Size.Length)
+ : 100.0;
+
+ foreach (var (subNum, pgm) in holeEntries)
+ {
+ CincinnatiPartSubprogramWriter.EnsureLeadingRapid(pgm);
+ holeSubWriter.Write(writer, pgm, "HOLE", subNum,
+ initialCutLibrary, etchLibrary, sheetDiagonal);
+ }
+ }
+
writer.Flush();
}
diff --git a/OpenNest.Posts.Cincinnati/CincinnatiSheetWriter.cs b/OpenNest.Posts.Cincinnati/CincinnatiSheetWriter.cs
index b2b9e9c..a8f4bc9 100644
--- a/OpenNest.Posts.Cincinnati/CincinnatiSheetWriter.cs
+++ b/OpenNest.Posts.Cincinnati/CincinnatiSheetWriter.cs
@@ -17,13 +17,16 @@ public sealed class CincinnatiSheetWriter
private readonly ProgramVariableManager _vars;
private readonly CoordinateFormatter _fmt;
private readonly CincinnatiFeatureWriter _featureWriter;
+ private readonly Dictionary _holeSubprograms;
- public CincinnatiSheetWriter(CincinnatiPostConfig config, ProgramVariableManager vars)
+ public CincinnatiSheetWriter(CincinnatiPostConfig config, ProgramVariableManager vars,
+ Dictionary holeSubprograms = null)
{
_config = config;
_vars = vars;
_fmt = new CoordinateFormatter(config.PostedAccuracy);
_featureWriter = new CincinnatiFeatureWriter(config);
+ _holeSubprograms = holeSubprograms;
}
///
@@ -132,11 +135,21 @@ public sealed class CincinnatiSheetWriter
for (var f = 0; f < features.Count; f++)
{
var (codes, isEtch) = features[f];
+ var isLastFeature = isLastPart && f == features.Count - 1;
+
+ // SubProgramCall features are emitted as M98 hole calls
+ if (codes.Count == 1 && codes[0] is SubProgramCall holeCall)
+ {
+ WriteHoleSubprogramCall(w, holeCall, featureIndex, isLastFeature);
+ featureIndex++;
+ lastPartName = partName;
+ continue;
+ }
+
var featureNumber = featureIndex == 0
? _config.FeatureLineNumberStart
: 1000 + featureIndex + 1;
- var isLastFeature = isLastPart && f == features.Count - 1;
var cutDistance = FeatureUtils.ComputeCutDistance(codes);
var ctx = new FeatureContext
@@ -204,6 +217,25 @@ public sealed class CincinnatiSheetWriter
w.WriteLine("M47");
}
+ private void WriteHoleSubprogramCall(TextWriter w, SubProgramCall call, int featureIndex, bool isLastFeature)
+ {
+ var postSubNum = _holeSubprograms != null && _holeSubprograms.TryGetValue(call.Id, out var num)
+ ? num : call.Id;
+
+ var featureNumber = featureIndex == 0
+ ? _config.FeatureLineNumberStart
+ : 1000 + featureIndex + 1;
+
+ var sb = new StringBuilder();
+ if (_config.UseLineNumbers)
+ sb.Append($"N{featureNumber} ");
+ sb.Append($"M98 P{postSubNum} X{_fmt.FormatCoord(call.Offset.X)} Y{_fmt.FormatCoord(call.Offset.Y)}");
+ w.WriteLine(sb.ToString());
+
+ if (!isLastFeature)
+ w.WriteLine("M47");
+ }
+
private void WritePartsInline(TextWriter w, List allParts,
string cutLibrary, string etchLibrary, double sheetDiagonal,
double plateWidth, double plateLength,
@@ -228,6 +260,14 @@ public sealed class CincinnatiSheetWriter
var isSafetyHeadraise = partName != lastPartName && lastPartName != "";
var isLastFeature = i == features.Count - 1;
+ // SubProgramCall features are emitted as M98 hole calls
+ if (codes.Count == 1 && codes[0] is SubProgramCall holeCall)
+ {
+ WriteHoleSubprogramCall(w, holeCall, i, isLastFeature);
+ lastPartName = partName;
+ continue;
+ }
+
var featureNumber = i == 0
? _config.FeatureLineNumberStart
: 1000 + i + 1;
diff --git a/OpenNest.Posts.Cincinnati/FeatureUtils.cs b/OpenNest.Posts.Cincinnati/FeatureUtils.cs
index d31d3a2..e75b5b2 100644
--- a/OpenNest.Posts.Cincinnati/FeatureUtils.cs
+++ b/OpenNest.Posts.Cincinnati/FeatureUtils.cs
@@ -21,7 +21,16 @@ public static class FeatureUtils
foreach (var code in codes)
{
- if (code is RapidMove)
+ if (code is SubProgramCall)
+ {
+ // Flush any pending feature
+ if (current != null)
+ features.Add(current);
+ // SubProgramCall is its own feature
+ features.Add(new List { code });
+ current = null;
+ }
+ else if (code is RapidMove)
{
if (current != null)
features.Add(current);