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:
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user