refactor: extract shared feature utilities and sub-program registry from CincinnatiPostProcessor

Consolidate duplicated static methods (SplitFeatures, ComputeCutDistance,
IsFeatureEtch, feature ordering) from CincinnatiSheetWriter and
CincinnatiPartSubprogramWriter into a shared FeatureUtils class. Move
inline sub-program registry building from Post() into
CincinnatiPartSubprogramWriter.BuildRegistry().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-25 21:54:04 -04:00
parent 1aac03c9ef
commit cbabf5e9d1
5 changed files with 151 additions and 187 deletions
@@ -29,12 +29,12 @@ public sealed class CincinnatiPartSubprogramWriter
public void Write(TextWriter w, Program normalizedProgram, string drawingName, public void Write(TextWriter w, Program normalizedProgram, string drawingName,
int subNumber, string cutLibrary, string etchLibrary, double sheetDiagonal) int subNumber, string cutLibrary, string etchLibrary, double sheetDiagonal)
{ {
var allFeatures = SplitFeatures(normalizedProgram.Codes); var allFeatures = FeatureUtils.SplitByRapids(normalizedProgram.Codes);
if (allFeatures.Count == 0) if (allFeatures.Count == 0)
return; return;
// Classify and order: etch features first, then cut features // Classify and order: etch features first, then cut features
var ordered = OrderFeatures(allFeatures); var ordered = FeatureUtils.ClassifyAndOrder(allFeatures);
w.WriteLine("(*****************************************************)"); w.WriteLine("(*****************************************************)");
w.WriteLine($":{subNumber}"); w.WriteLine($":{subNumber}");
@@ -46,7 +46,7 @@ public sealed class CincinnatiPartSubprogramWriter
var featureNumber = i == 0 var featureNumber = i == 0
? _config.FeatureLineNumberStart ? _config.FeatureLineNumberStart
: 1000 + i + 1; : 1000 + i + 1;
var cutDistance = ComputeCutDistance(codes); var cutDistance = FeatureUtils.ComputeCutDistance(codes);
var ctx = new FeatureContext var ctx = new FeatureContext
{ {
@@ -70,81 +70,43 @@ public sealed class CincinnatiPartSubprogramWriter
w.WriteLine($"M99 (END OF {drawingName})"); w.WriteLine($"M99 (END OF {drawingName})");
} }
internal static List<(List<ICode> codes, bool isEtch)> OrderFeatures(List<List<ICode>> features)
{
var result = new List<(List<ICode>, bool)>();
var etch = new List<List<ICode>>();
var cut = new List<List<ICode>>();
foreach (var f in features)
{
if (CincinnatiSheetWriter.IsFeatureEtch(f))
etch.Add(f);
else
cut.Add(f);
}
foreach (var f in etch)
result.Add((f, true));
foreach (var f in cut)
result.Add((f, false));
return result;
}
/// <summary> /// <summary>
/// Creates a sub-program key for matching parts to their sub-programs. /// Creates a sub-program key for matching parts to their sub-programs.
/// </summary> /// </summary>
internal static (int drawingId, long rotationKey) SubprogramKey(Part part) => internal static (int drawingId, long rotationKey) SubprogramKey(Part part) =>
(part.BaseDrawing.Id, (long)System.Math.Round(part.Rotation * 1e6)); (part.BaseDrawing.Id, (long)System.Math.Round(part.Rotation * 1e6));
internal static List<List<ICode>> SplitFeatures(List<ICode> codes) /// <summary>
/// Scans all plates and builds a mapping of unique part geometries to sub-program numbers,
/// along with their normalized programs for writing.
/// </summary>
internal static (Dictionary<(int, long), int> mapping, List<(int subNum, string name, Program program)> entries)
BuildRegistry(IEnumerable<Plate> plates, int startNumber)
{ {
var features = new List<List<ICode>>(); var mapping = new Dictionary<(int, long), int>();
List<ICode> current = null; var entries = new List<(int, string, Program)>();
var nextSubNum = startNumber;
foreach (var code in codes) foreach (var plate in plates)
{ {
if (code is RapidMove) foreach (var part in plate.Parts)
{ {
if (current != null) if (part.BaseDrawing.IsCutOff) continue;
features.Add(current); var key = SubprogramKey(part);
current = new List<ICode> { code }; if (!mapping.ContainsKey(key))
} {
else var subNum = nextSubNum++;
{ mapping[key] = subNum;
current ??= new List<ICode>();
current.Add(code); var pgm = part.Program.Clone() as Program;
var bbox = pgm.BoundingBox();
pgm.Offset(-bbox.Location.X, -bbox.Location.Y);
entries.Add((subNum, part.BaseDrawing.Name, pgm));
}
} }
} }
if (current != null && current.Count > 0) return (mapping, entries);
features.Add(current);
return features;
}
internal static double ComputeCutDistance(List<ICode> codes)
{
var distance = 0.0;
var currentPos = Vector.Zero;
foreach (var code in codes)
{
if (code is RapidMove rapid)
currentPos = rapid.EndPoint;
else if (code is LinearMove linear)
{
distance += currentPos.DistanceTo(linear.EndPoint);
currentPos = linear.EndPoint;
}
else if (code is ArcMove arc)
{
distance += currentPos.DistanceTo(arc.EndPoint);
currentPos = arc.EndPoint;
}
}
return distance;
} }
} }
@@ -84,32 +84,7 @@ namespace OpenNest.Posts.Cincinnati
List<(int subNum, string name, Program program)> subprogramEntries = null; List<(int subNum, string name, Program program)> subprogramEntries = null;
if (Config.UsePartSubprograms) if (Config.UsePartSubprograms)
{ (partSubprograms, subprogramEntries) = CincinnatiPartSubprogramWriter.BuildRegistry(plates, Config.PartSubprogramStart);
partSubprograms = new Dictionary<(int, long), int>();
subprogramEntries = new List<(int, string, Program)>();
var nextSubNum = Config.PartSubprogramStart;
foreach (var plate in plates)
{
foreach (var part in plate.Parts)
{
if (part.BaseDrawing.IsCutOff) continue;
var key = CincinnatiPartSubprogramWriter.SubprogramKey(part);
if (!partSubprograms.ContainsKey(key))
{
var subNum = nextSubNum++;
partSubprograms[key] = subNum;
// Create normalized program at origin
var pgm = part.Program.Clone() as Program;
var bbox = pgm.BoundingBox();
pgm.Offset(-bbox.Location.X, -bbox.Location.Y);
subprogramEntries.Add((subNum, part.BaseDrawing.Name, pgm));
}
}
}
}
// 5. Create writers // 5. Create writers
var preamble = new CincinnatiPreambleWriter(Config); var preamble = new CincinnatiPreambleWriter(Config);
@@ -4,7 +4,6 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using OpenNest.CNC; using OpenNest.CNC;
using OpenNest.Geometry;
namespace OpenNest.Posts.Cincinnati; namespace OpenNest.Posts.Cincinnati;
@@ -128,7 +127,7 @@ public sealed class CincinnatiSheetWriter
else else
{ {
// Inline features for cutoffs or parts without sub-programs // Inline features for cutoffs or parts without sub-programs
var features = SplitAndOrderFeatures(part); var features = FeatureUtils.SplitAndClassify(part);
for (var f = 0; f < features.Count; f++) for (var f = 0; f < features.Count; f++)
{ {
var (codes, isEtch) = features[f]; var (codes, isEtch) = features[f];
@@ -137,7 +136,7 @@ public sealed class CincinnatiSheetWriter
: 1000 + featureIndex + 1; : 1000 + featureIndex + 1;
var isLastFeature = isLastPart && f == features.Count - 1; var isLastFeature = isLastPart && f == features.Count - 1;
var cutDistance = ComputeCutDistance(codes); var cutDistance = FeatureUtils.ComputeCutDistance(codes);
var ctx = new FeatureContext var ctx = new FeatureContext
{ {
@@ -205,7 +204,7 @@ public sealed class CincinnatiSheetWriter
var features = new List<(Part part, List<ICode> codes, bool isEtch)>(); var features = new List<(Part part, List<ICode> codes, bool isEtch)>();
foreach (var part in allParts) foreach (var part in allParts)
{ {
var partFeatures = SplitAndOrderFeatures(part); var partFeatures = FeatureUtils.SplitAndClassify(part);
foreach (var (codes, isEtch) in partFeatures) foreach (var (codes, isEtch) in partFeatures)
features.Add((part, codes, isEtch)); features.Add((part, codes, isEtch));
} }
@@ -224,7 +223,7 @@ public sealed class CincinnatiSheetWriter
? _config.FeatureLineNumberStart ? _config.FeatureLineNumberStart
: 1000 + i + 1; : 1000 + i + 1;
var cutDistance = ComputeCutDistance(codes); var cutDistance = FeatureUtils.ComputeCutDistance(codes);
var ctx = new FeatureContext var ctx = new FeatureContext
{ {
@@ -246,91 +245,4 @@ public sealed class CincinnatiSheetWriter
} }
} }
/// <summary>
/// Splits a part's program into features (by rapids), classifies each as etch or cut,
/// and orders etch features before cut features.
/// </summary>
public static List<(List<ICode> codes, bool isEtch)> SplitAndOrderFeatures(Part part)
{
var etchFeatures = new List<List<ICode>>();
var cutFeatures = new List<List<ICode>>();
List<ICode> current = null;
foreach (var code in part.Program.Codes)
{
if (code is RapidMove)
{
if (current != null)
ClassifyAndAdd(current, etchFeatures, cutFeatures);
current = new List<ICode> { code };
}
else
{
current ??= new List<ICode>();
current.Add(code);
}
}
if (current != null && current.Count > 0)
ClassifyAndAdd(current, etchFeatures, cutFeatures);
// Etch features first, then cut features
var result = new List<(List<ICode>, bool)>();
foreach (var f in etchFeatures)
result.Add((f, true));
foreach (var f in cutFeatures)
result.Add((f, false));
return result;
}
private static void ClassifyAndAdd(List<ICode> codes,
List<List<ICode>> etchFeatures, List<List<ICode>> cutFeatures)
{
if (IsFeatureEtch(codes))
etchFeatures.Add(codes);
else
cutFeatures.Add(codes);
}
/// <summary>
/// A feature is etch if any non-rapid move has LayerType.Scribe.
/// </summary>
public static bool IsFeatureEtch(List<ICode> codes)
{
foreach (var code in codes)
{
if (code is LinearMove linear && linear.Layer == LayerType.Scribe)
return true;
if (code is ArcMove arc && arc.Layer == LayerType.Scribe)
return true;
}
return false;
}
private static double ComputeCutDistance(List<ICode> codes)
{
var distance = 0.0;
var currentPos = Vector.Zero;
foreach (var code in codes)
{
if (code is RapidMove rapid)
{
currentPos = rapid.EndPoint;
}
else if (code is LinearMove linear)
{
distance += currentPos.DistanceTo(linear.EndPoint);
currentPos = linear.EndPoint;
}
else if (code is ArcMove arc)
{
distance += currentPos.DistanceTo(arc.EndPoint);
currentPos = arc.EndPoint;
}
}
return distance;
}
} }
+115
View File
@@ -0,0 +1,115 @@
using System.Collections.Generic;
using OpenNest.CNC;
using OpenNest.Geometry;
namespace OpenNest.Posts.Cincinnati;
/// <summary>
/// Shared utilities for splitting CNC programs into features and classifying them.
/// </summary>
public static class FeatureUtils
{
/// <summary>
/// Splits a flat list of codes into feature groups, breaking on rapid moves.
/// Each feature starts with a rapid move followed by cutting/etching moves.
/// </summary>
public static List<List<ICode>> SplitByRapids(List<ICode> codes)
{
var features = new List<List<ICode>>();
List<ICode> current = null;
foreach (var code in codes)
{
if (code is RapidMove)
{
if (current != null)
features.Add(current);
current = new List<ICode> { code };
}
else
{
current ??= new List<ICode>();
current.Add(code);
}
}
if (current != null && current.Count > 0)
features.Add(current);
return features;
}
/// <summary>
/// Classifies features as etch or cut and orders etch features before cut features.
/// </summary>
public static List<(List<ICode> codes, bool isEtch)> ClassifyAndOrder(List<List<ICode>> features)
{
var result = new List<(List<ICode>, bool)>();
var etch = new List<List<ICode>>();
var cut = new List<List<ICode>>();
foreach (var f in features)
{
if (IsEtch(f))
etch.Add(f);
else
cut.Add(f);
}
foreach (var f in etch)
result.Add((f, true));
foreach (var f in cut)
result.Add((f, false));
return result;
}
/// <summary>
/// Splits a part's program into features by rapids, classifies each as etch or cut,
/// and orders etch features before cut features.
/// </summary>
public static List<(List<ICode> codes, bool isEtch)> SplitAndClassify(Part part) =>
ClassifyAndOrder(SplitByRapids(part.Program.Codes));
/// <summary>
/// Returns true if any non-rapid move in the feature has LayerType.Scribe.
/// </summary>
public static bool IsEtch(List<ICode> codes)
{
foreach (var code in codes)
{
if (code is LinearMove linear && linear.Layer == LayerType.Scribe)
return true;
if (code is ArcMove arc && arc.Layer == LayerType.Scribe)
return true;
}
return false;
}
/// <summary>
/// Computes the total cut distance of a feature by summing segment lengths.
/// </summary>
public static double ComputeCutDistance(List<ICode> codes)
{
var distance = 0.0;
var currentPos = Vector.Zero;
foreach (var code in codes)
{
if (code is RapidMove rapid)
currentPos = rapid.EndPoint;
else if (code is LinearMove linear)
{
distance += currentPos.DistanceTo(linear.EndPoint);
currentPos = linear.EndPoint;
}
else if (code is ArcMove arc)
{
distance += currentPos.DistanceTo(arc.EndPoint);
currentPos = arc.EndPoint;
}
}
return distance;
}
}
@@ -152,7 +152,7 @@ public class CincinnatiSheetWriterTests
new LinearMove(1, 1) { Layer = LayerType.Scribe } new LinearMove(1, 1) { Layer = LayerType.Scribe }
}; };
Assert.True(CincinnatiSheetWriter.IsFeatureEtch(codes)); Assert.True(FeatureUtils.IsEtch(codes));
} }
[Fact] [Fact]
@@ -165,7 +165,7 @@ public class CincinnatiSheetWriterTests
new LinearMove(1, 1) { Layer = LayerType.Cut } new LinearMove(1, 1) { Layer = LayerType.Cut }
}; };
Assert.False(CincinnatiSheetWriter.IsFeatureEtch(codes)); Assert.False(FeatureUtils.IsEtch(codes));
} }
[Fact] [Fact]
@@ -176,7 +176,7 @@ public class CincinnatiSheetWriterTests
new RapidMove(0, 0) new RapidMove(0, 0)
}; };
Assert.False(CincinnatiSheetWriter.IsFeatureEtch(codes)); Assert.False(FeatureUtils.IsEtch(codes));
} }
private static Program CreateSimpleProgram() private static Program CreateSimpleProgram()