From 09eac96a0352e961d5b236b388d14e9de7b11451 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Thu, 9 Apr 2026 14:44:58 -0400 Subject: [PATCH] feat: handle SubProgramCalls in Cincinnati post feature splitting SubProgramCalls are now treated as standalone features in the Cincinnati post-processor. SplitByRapids emits them as single-element features instead of splitting on rapids within sub-programs. A nest-level hole sub-program registry deduplicates by content and assigns post numbers. Sheet writers emit M98 calls with X/Y offsets for hole features, and hole sub-program definitions are written after part sub-programs. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../CincinnatiPartSubprogramWriter.cs | 58 +++++++++++++++++++ .../CincinnatiPostProcessor.cs | 25 +++++++- .../CincinnatiSheetWriter.cs | 44 +++++++++++++- OpenNest.Posts.Cincinnati/FeatureUtils.cs | 11 +++- 4 files changed, 134 insertions(+), 4 deletions(-) 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);