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:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -17,13 +17,16 @@ public sealed class CincinnatiSheetWriter
|
||||
private readonly ProgramVariableManager _vars;
|
||||
private readonly CoordinateFormatter _fmt;
|
||||
private readonly CincinnatiFeatureWriter _featureWriter;
|
||||
private readonly Dictionary<int, int> _holeSubprograms;
|
||||
|
||||
public CincinnatiSheetWriter(CincinnatiPostConfig config, ProgramVariableManager vars)
|
||||
public CincinnatiSheetWriter(CincinnatiPostConfig config, ProgramVariableManager vars,
|
||||
Dictionary<int, int> holeSubprograms = null)
|
||||
{
|
||||
_config = config;
|
||||
_vars = vars;
|
||||
_fmt = new CoordinateFormatter(config.PostedAccuracy);
|
||||
_featureWriter = new CincinnatiFeatureWriter(config);
|
||||
_holeSubprograms = holeSubprograms;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -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<Part> 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;
|
||||
|
||||
@@ -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<ICode> { code });
|
||||
current = null;
|
||||
}
|
||||
else if (code is RapidMove)
|
||||
{
|
||||
if (current != null)
|
||||
features.Add(current);
|
||||
|
||||
Reference in New Issue
Block a user