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) <noreply@anthropic.com>
This commit is contained in:
2026-04-09 14:44:58 -04:00
parent df65414a9d
commit 09eac96a03
4 changed files with 134 additions and 4 deletions

View File

@@ -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);
}
/// <summary>
/// Scans all parts across all plates and builds a nest-level registry of unique
/// hole sub-programs. Deduplicates by comparing sub-program code content.
/// </summary>
internal static (Dictionary<int, int> modelToPostMapping, List<(int subNum, Program program)> entries)
BuildHoleRegistry(IEnumerable<Plate> plates, int startNumber)
{
var mapping = new Dictionary<int, int>();
var entries = new List<(int, Program)>();
var contentIndex = new Dictionary<string, int>();
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();
}
}