From cbabf5e9d1513fe9cd98e78ff7650e12afa4adda Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Wed, 25 Mar 2026 21:54:04 -0400 Subject: [PATCH] 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) --- .../CincinnatiPartSubprogramWriter.cs | 94 +++++--------- .../CincinnatiPostProcessor.cs | 27 +--- .../CincinnatiSheetWriter.cs | 96 +-------------- OpenNest.Posts.Cincinnati/FeatureUtils.cs | 115 ++++++++++++++++++ .../Cincinnati/CincinnatiSheetWriterTests.cs | 6 +- 5 files changed, 151 insertions(+), 187 deletions(-) create mode 100644 OpenNest.Posts.Cincinnati/FeatureUtils.cs diff --git a/OpenNest.Posts.Cincinnati/CincinnatiPartSubprogramWriter.cs b/OpenNest.Posts.Cincinnati/CincinnatiPartSubprogramWriter.cs index de46883..f08f62e 100644 --- a/OpenNest.Posts.Cincinnati/CincinnatiPartSubprogramWriter.cs +++ b/OpenNest.Posts.Cincinnati/CincinnatiPartSubprogramWriter.cs @@ -29,12 +29,12 @@ public sealed class CincinnatiPartSubprogramWriter public void Write(TextWriter w, Program normalizedProgram, string drawingName, int subNumber, string cutLibrary, string etchLibrary, double sheetDiagonal) { - var allFeatures = SplitFeatures(normalizedProgram.Codes); + var allFeatures = FeatureUtils.SplitByRapids(normalizedProgram.Codes); if (allFeatures.Count == 0) return; // Classify and order: etch features first, then cut features - var ordered = OrderFeatures(allFeatures); + var ordered = FeatureUtils.ClassifyAndOrder(allFeatures); w.WriteLine("(*****************************************************)"); w.WriteLine($":{subNumber}"); @@ -46,7 +46,7 @@ public sealed class CincinnatiPartSubprogramWriter var featureNumber = i == 0 ? _config.FeatureLineNumberStart : 1000 + i + 1; - var cutDistance = ComputeCutDistance(codes); + var cutDistance = FeatureUtils.ComputeCutDistance(codes); var ctx = new FeatureContext { @@ -70,81 +70,43 @@ public sealed class CincinnatiPartSubprogramWriter w.WriteLine($"M99 (END OF {drawingName})"); } - internal static List<(List codes, bool isEtch)> OrderFeatures(List> features) - { - var result = new List<(List, bool)>(); - var etch = new List>(); - var cut = new List>(); - - 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; - } - /// /// Creates a sub-program key for matching parts to their sub-programs. /// internal static (int drawingId, long rotationKey) SubprogramKey(Part part) => (part.BaseDrawing.Id, (long)System.Math.Round(part.Rotation * 1e6)); - internal static List> SplitFeatures(List codes) + /// + /// Scans all plates and builds a mapping of unique part geometries to sub-program numbers, + /// along with their normalized programs for writing. + /// + internal static (Dictionary<(int, long), int> mapping, List<(int subNum, string name, Program program)> entries) + BuildRegistry(IEnumerable plates, int startNumber) { - var features = new List>(); - List current = null; + var mapping = new Dictionary<(int, long), int>(); + 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) - features.Add(current); - current = new List { code }; - } - else - { - current ??= new List(); - current.Add(code); + if (part.BaseDrawing.IsCutOff) continue; + var key = SubprogramKey(part); + if (!mapping.ContainsKey(key)) + { + var subNum = nextSubNum++; + mapping[key] = subNum; + + 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) - features.Add(current); - - return features; - } - - internal static double ComputeCutDistance(List 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; + return (mapping, entries); } } diff --git a/OpenNest.Posts.Cincinnati/CincinnatiPostProcessor.cs b/OpenNest.Posts.Cincinnati/CincinnatiPostProcessor.cs index 41db93c..4f764ff 100644 --- a/OpenNest.Posts.Cincinnati/CincinnatiPostProcessor.cs +++ b/OpenNest.Posts.Cincinnati/CincinnatiPostProcessor.cs @@ -84,32 +84,7 @@ namespace OpenNest.Posts.Cincinnati List<(int subNum, string name, Program program)> subprogramEntries = null; if (Config.UsePartSubprograms) - { - 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)); - } - } - } - } + (partSubprograms, subprogramEntries) = CincinnatiPartSubprogramWriter.BuildRegistry(plates, Config.PartSubprogramStart); // 5. Create writers var preamble = new CincinnatiPreambleWriter(Config); diff --git a/OpenNest.Posts.Cincinnati/CincinnatiSheetWriter.cs b/OpenNest.Posts.Cincinnati/CincinnatiSheetWriter.cs index 42220e8..ad1fb13 100644 --- a/OpenNest.Posts.Cincinnati/CincinnatiSheetWriter.cs +++ b/OpenNest.Posts.Cincinnati/CincinnatiSheetWriter.cs @@ -4,7 +4,6 @@ using System.IO; using System.Linq; using System.Text; using OpenNest.CNC; -using OpenNest.Geometry; namespace OpenNest.Posts.Cincinnati; @@ -128,7 +127,7 @@ public sealed class CincinnatiSheetWriter else { // 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++) { var (codes, isEtch) = features[f]; @@ -137,7 +136,7 @@ public sealed class CincinnatiSheetWriter : 1000 + featureIndex + 1; var isLastFeature = isLastPart && f == features.Count - 1; - var cutDistance = ComputeCutDistance(codes); + var cutDistance = FeatureUtils.ComputeCutDistance(codes); var ctx = new FeatureContext { @@ -205,7 +204,7 @@ public sealed class CincinnatiSheetWriter var features = new List<(Part part, List codes, bool isEtch)>(); foreach (var part in allParts) { - var partFeatures = SplitAndOrderFeatures(part); + var partFeatures = FeatureUtils.SplitAndClassify(part); foreach (var (codes, isEtch) in partFeatures) features.Add((part, codes, isEtch)); } @@ -224,7 +223,7 @@ public sealed class CincinnatiSheetWriter ? _config.FeatureLineNumberStart : 1000 + i + 1; - var cutDistance = ComputeCutDistance(codes); + var cutDistance = FeatureUtils.ComputeCutDistance(codes); var ctx = new FeatureContext { @@ -246,91 +245,4 @@ public sealed class CincinnatiSheetWriter } } - /// - /// Splits a part's program into features (by rapids), classifies each as etch or cut, - /// and orders etch features before cut features. - /// - public static List<(List codes, bool isEtch)> SplitAndOrderFeatures(Part part) - { - var etchFeatures = new List>(); - var cutFeatures = new List>(); - List current = null; - - foreach (var code in part.Program.Codes) - { - if (code is RapidMove) - { - if (current != null) - ClassifyAndAdd(current, etchFeatures, cutFeatures); - current = new List { code }; - } - else - { - current ??= new List(); - current.Add(code); - } - } - - if (current != null && current.Count > 0) - ClassifyAndAdd(current, etchFeatures, cutFeatures); - - // Etch features first, then cut features - var result = new List<(List, 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 codes, - List> etchFeatures, List> cutFeatures) - { - if (IsFeatureEtch(codes)) - etchFeatures.Add(codes); - else - cutFeatures.Add(codes); - } - - /// - /// A feature is etch if any non-rapid move has LayerType.Scribe. - /// - public static bool IsFeatureEtch(List 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 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; - } } diff --git a/OpenNest.Posts.Cincinnati/FeatureUtils.cs b/OpenNest.Posts.Cincinnati/FeatureUtils.cs new file mode 100644 index 0000000..8fb3759 --- /dev/null +++ b/OpenNest.Posts.Cincinnati/FeatureUtils.cs @@ -0,0 +1,115 @@ +using System.Collections.Generic; +using OpenNest.CNC; +using OpenNest.Geometry; + +namespace OpenNest.Posts.Cincinnati; + +/// +/// Shared utilities for splitting CNC programs into features and classifying them. +/// +public static class FeatureUtils +{ + /// + /// 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. + /// + public static List> SplitByRapids(List codes) + { + var features = new List>(); + List current = null; + + foreach (var code in codes) + { + if (code is RapidMove) + { + if (current != null) + features.Add(current); + current = new List { code }; + } + else + { + current ??= new List(); + current.Add(code); + } + } + + if (current != null && current.Count > 0) + features.Add(current); + + return features; + } + + /// + /// Classifies features as etch or cut and orders etch features before cut features. + /// + public static List<(List codes, bool isEtch)> ClassifyAndOrder(List> features) + { + var result = new List<(List, bool)>(); + var etch = new List>(); + var cut = new List>(); + + 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; + } + + /// + /// Splits a part's program into features by rapids, classifies each as etch or cut, + /// and orders etch features before cut features. + /// + public static List<(List codes, bool isEtch)> SplitAndClassify(Part part) => + ClassifyAndOrder(SplitByRapids(part.Program.Codes)); + + /// + /// Returns true if any non-rapid move in the feature has LayerType.Scribe. + /// + public static bool IsEtch(List 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; + } + + /// + /// Computes the total cut distance of a feature by summing segment lengths. + /// + public static double ComputeCutDistance(List 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; + } +} diff --git a/OpenNest.Tests/Cincinnati/CincinnatiSheetWriterTests.cs b/OpenNest.Tests/Cincinnati/CincinnatiSheetWriterTests.cs index 24841f7..7d94cc4 100644 --- a/OpenNest.Tests/Cincinnati/CincinnatiSheetWriterTests.cs +++ b/OpenNest.Tests/Cincinnati/CincinnatiSheetWriterTests.cs @@ -152,7 +152,7 @@ public class CincinnatiSheetWriterTests new LinearMove(1, 1) { Layer = LayerType.Scribe } }; - Assert.True(CincinnatiSheetWriter.IsFeatureEtch(codes)); + Assert.True(FeatureUtils.IsEtch(codes)); } [Fact] @@ -165,7 +165,7 @@ public class CincinnatiSheetWriterTests new LinearMove(1, 1) { Layer = LayerType.Cut } }; - Assert.False(CincinnatiSheetWriter.IsFeatureEtch(codes)); + Assert.False(FeatureUtils.IsEtch(codes)); } [Fact] @@ -176,7 +176,7 @@ public class CincinnatiSheetWriterTests new RapidMove(0, 0) }; - Assert.False(CincinnatiSheetWriter.IsFeatureEtch(codes)); + Assert.False(FeatureUtils.IsEtch(codes)); } private static Program CreateSimpleProgram()