Compare commits

...

5 Commits

Author SHA1 Message Date
aj b970629a59 feat: add material library resolution, assist gas support, and UI fixes
- Add MaterialLibraryResolver for Cincinnati post processor to resolve
  G89 library files from material/thickness/gas configuration
- Add Nest.AssistGas property with serialization support in nest format
- Add etch library support with separate gas configuration
- Fix CutOff tests to match AwayFromOrigin default cut direction
- Fix plate info label not updating after ResizePlateToFitParts
- Add cutoff and remnants toolbar button icons

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 22:00:57 -04:00
aj 072915abf2 fix: detect winding direction for correct part spacing offset
PolygonHelper.ExtractPerimeterPolygon always used OffsetSide.Right
assuming CCW winding, but DXF imports can produce CW winding. This
caused the spacing polygon to shrink inward instead of expanding
outward, making parts overlap during nesting.

Now detects winding direction via polygon signed area and selects
the correct OffsetSide accordingly.

Also adds save_nest MCP tool and a BOM-to-nest builder utility
(tools/NestBuilder) for batch-creating nest files from Excel BOMs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 21:57:23 -04:00
aj aeeb2e4074 fix: treat cut-offs as area selection boundaries with proper spacing
Cut-off parts use absolute coordinates in their programs, causing
Program.BoundingBox() to span from the origin to the cut-off position.
This made cut-offs invisible to GetLargestBoxVertically/Horizontally
since the oversized box straddled the cursor instead of acting as a
boundary. Derive thin obstacle boxes directly from CutOff definitions
and apply PartSpacing offset so fills respect spacing from cut lines.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 12:14:58 -04:00
aj a2f7219db3 fix: add proper spacing between G-code words in Cincinnati post output
G-code output was concatenated without spaces (e.g. N1005G0X1.4375Y-0.6562).
Now emits standard spacing (N1005 G0 X1.4375 Y-0.6562) across all motion
commands, line numbers, kerf comp, feedrates, M-codes, and comments.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 05:46:46 -04:00
aj 7e4040ba08 feat: add post processor support to console app
Load IPostProcessor plugin DLLs from Posts/ directory (same convention
as the WinForms app) and run them after nesting via --post <name>.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 01:51:45 -04:00
31 changed files with 1124 additions and 166 deletions
+122
View File
@@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
return NestConsole.Run(args);
@@ -20,6 +21,12 @@ static class NestConsole
if (options == null)
return 0; // --help was requested
if (options.ListPosts)
{
ListPostProcessors(options);
return 0;
}
if (options.InputFiles.Count == 0)
{
PrintUsage();
@@ -68,6 +75,7 @@ static class NestConsole
PrintResults(success, plate, elapsed);
Save(nest, options);
PostProcess(nest, options);
return options.CheckOverlaps && overlapCount > 0 ? 1 : 0;
}
@@ -120,6 +128,18 @@ static class NestConsole
case "--engine" when i + 1 < args.Length:
NestEngineRegistry.ActiveEngineName = args[++i];
break;
case "--post" when i + 1 < args.Length:
o.PostName = args[++i];
break;
case "--post-output" when i + 1 < args.Length:
o.PostOutput = args[++i];
break;
case "--posts-dir" when i + 1 < args.Length:
o.PostsDir = args[++i];
break;
case "--list-posts":
o.ListPosts = true;
break;
case "--help":
case "-h":
PrintUsage();
@@ -382,6 +402,100 @@ static class NestConsole
Console.WriteLine($"Saved: {outputFile}");
}
static string ResolvePostsDir(Options options)
{
if (options.PostsDir != null)
return options.PostsDir;
var exePath = Assembly.GetEntryAssembly()?.Location
?? typeof(NestConsole).Assembly.Location;
return Path.Combine(Path.GetDirectoryName(exePath), "Posts");
}
static List<IPostProcessor> LoadPostProcessors(string postsDir)
{
var processors = new List<IPostProcessor>();
if (!Directory.Exists(postsDir))
return processors;
foreach (var file in Directory.GetFiles(postsDir, "*.dll"))
{
try
{
var assembly = Assembly.LoadFrom(file);
foreach (var type in assembly.GetTypes())
{
if (!typeof(IPostProcessor).IsAssignableFrom(type) || type.IsInterface || type.IsAbstract)
continue;
if (Activator.CreateInstance(type) is IPostProcessor processor)
processors.Add(processor);
}
}
catch (Exception ex)
{
Console.Error.WriteLine($"Warning: failed to load post processor from {Path.GetFileName(file)}: {ex.Message}");
}
}
return processors;
}
static void ListPostProcessors(Options options)
{
var postsDir = ResolvePostsDir(options);
var processors = LoadPostProcessors(postsDir);
if (processors.Count == 0)
{
Console.WriteLine($"No post processors found in: {postsDir}");
return;
}
Console.WriteLine($"Post processors ({postsDir}):");
foreach (var p in processors)
Console.WriteLine($" {p.Name,-30} {p.Description}");
}
static void PostProcess(Nest nest, Options options)
{
if (options.PostName == null)
return;
var postsDir = ResolvePostsDir(options);
var processors = LoadPostProcessors(postsDir);
var post = processors.FirstOrDefault(p =>
p.Name.Equals(options.PostName, StringComparison.OrdinalIgnoreCase));
if (post == null)
{
Console.Error.WriteLine($"Error: post processor '{options.PostName}' not found");
if (processors.Count > 0)
Console.Error.WriteLine($"Available: {string.Join(", ", processors.Select(p => p.Name))}");
else
Console.Error.WriteLine($"No post processors found in: {postsDir}");
return;
}
var outputFile = options.PostOutput;
if (outputFile == null)
{
var firstInput = options.InputFiles[0];
outputFile = Path.Combine(
Path.GetDirectoryName(firstInput),
$"{Path.GetFileNameWithoutExtension(firstInput)}.cnc");
}
post.Post(nest, outputFile);
Console.WriteLine($"Post: {post.Name} -> {outputFile}");
}
static void PrintUsage()
{
Console.Error.WriteLine("Usage: OpenNest.Console <input-files...> [options]");
@@ -407,6 +521,10 @@ static class NestConsole
Console.Error.WriteLine(" --check-overlaps Run overlap detection after fill (exit code 1 if found)");
Console.Error.WriteLine(" --no-save Skip saving output file");
Console.Error.WriteLine(" --no-log Skip writing debug log file");
Console.Error.WriteLine(" --post <name> Run a post processor after nesting");
Console.Error.WriteLine(" --post-output <path> Output file for post processor (default: <input>.cnc)");
Console.Error.WriteLine(" --posts-dir <path> Directory containing post processor DLLs (default: Posts/)");
Console.Error.WriteLine(" --list-posts List available post processors and exit");
Console.Error.WriteLine(" -h, --help Show this help");
}
@@ -425,5 +543,9 @@ static class NestConsole
public bool KeepParts;
public bool AutoNest;
public string TemplateFile;
public string PostName;
public string PostOutput;
public string PostsDir;
public bool ListPosts;
}
}
+2
View File
@@ -36,6 +36,8 @@ namespace OpenNest
public string Notes { get; set; }
public string AssistGas { get; set; } = "";
public Units Units { get; set; }
public DateTime DateCreated { get; set; }
+10 -2
View File
@@ -23,9 +23,17 @@ namespace OpenNest.Engine.BestFit
return new PolygonExtractionResult(null, Vector.Zero);
// Inflate by half-spacing if spacing is non-zero.
// OffsetSide.Right = outward for CCW perimeters (standard for outer contours).
// Detect winding direction to choose the correct outward offset side.
var outwardSide = OffsetSide.Right;
if (halfSpacing > 0)
{
var testPoly = perimeter.ToPolygon();
if (testPoly.Vertices.Count >= 3 && testPoly.RotationDirection() == RotationType.CW)
outwardSide = OffsetSide.Left;
}
var inflated = halfSpacing > 0
? (perimeter.OffsetEntity(halfSpacing, OffsetSide.Right) as Shape ?? perimeter)
? (perimeter.OffsetEntity(halfSpacing, outwardSide) as Shape ?? perimeter)
: perimeter;
// Convert to polygon with circumscribed arcs for tight nesting.
+1
View File
@@ -23,6 +23,7 @@ namespace OpenNest.IO
public string DateCreated { get; init; } = "";
public string DateLastModified { get; init; } = "";
public string Notes { get; init; } = "";
public string AssistGas { get; init; } = "";
public PlateDefaultsDto PlateDefaults { get; init; } = new();
public List<DrawingDto> Drawings { get; init; } = new();
public List<PlateDto> Plates { get; init; } = new();
+1
View File
@@ -160,6 +160,7 @@ namespace OpenNest.IO
nest.DateCreated = DateTime.Parse(dto.DateCreated);
nest.DateLastModified = DateTime.Parse(dto.DateLastModified);
nest.Notes = dto.Notes;
nest.AssistGas = dto.AssistGas ?? "";
// Plate defaults
var pd = dto.PlateDefaults;
+1
View File
@@ -77,6 +77,7 @@ namespace OpenNest.IO
DateCreated = nest.DateCreated.ToString("o"),
DateLastModified = nest.DateLastModified.ToString("o"),
Notes = nest.Notes ?? "",
AssistGas = nest.AssistGas ?? "",
PlateDefaults = BuildPlateDefaultsDto(),
Drawings = BuildDrawingDtos(),
Plates = BuildPlateDtos()
+29
View File
@@ -57,6 +57,35 @@ namespace OpenNest.Mcp.Tools
return sb.ToString();
}
[McpServerTool(Name = "save_nest")]
[Description("Save the current session (all drawings and plates) to a .nest file.")]
public string SaveNest(
[Description("Absolute path for the output .nest file")] string path,
[Description("Name for the nest (optional)")] string name = null)
{
var nest = new Nest();
nest.Name = name ?? Path.GetFileNameWithoutExtension(path);
foreach (var drawing in _session.AllDrawings())
nest.Drawings.Add(drawing);
foreach (var plate in _session.AllPlates())
nest.Plates.Add(plate);
if (nest.Drawings.Count == 0)
return "Error: no drawings in session to save";
var dir = Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
Directory.CreateDirectory(dir);
var writer = new NestWriter(nest);
if (!writer.Write(path))
return "Error: failed to write nest file";
return $"Saved nest to {path}\n Drawings: {nest.Drawings.Count}\n Plates: {nest.Plates.Count}";
}
[McpServerTool(Name = "import_dxf")]
[Description("Import a DXF file as a new drawing. Returns drawing name and bounding box.")]
public string ImportDxf(
@@ -19,6 +19,7 @@ public sealed class FeatureContext
public bool IsLastFeatureOnSheet { get; set; }
public bool IsSafetyHeadraise { get; set; }
public bool IsExteriorFeature { get; set; }
public bool IsEtch { get; set; }
public string LibraryFile { get; set; } = "";
public double CutDistance { get; set; }
public double SheetDiagonal { get; set; }
@@ -61,17 +62,24 @@ public sealed class CincinnatiFeatureWriter
if (ctx.IsFirstFeatureOfPart && !string.IsNullOrEmpty(ctx.PartName))
writer.WriteLine(CoordinateFormatter.Comment($"PART: {ctx.PartName}"));
// 3. G89 process params (if RepeatG89BeforeEachFeature)
if (_config.RepeatG89BeforeEachFeature && _config.ProcessParameterMode == G89Mode.LibraryFile)
// 3. G89 process params
if (_config.ProcessParameterMode == G89Mode.LibraryFile)
{
var lib = !string.IsNullOrEmpty(ctx.LibraryFile) ? ctx.LibraryFile : _config.DefaultLibraryFile;
var speedClass = _speedClassifier.Classify(ctx.CutDistance, ctx.SheetDiagonal);
var cutDist = _speedClassifier.FormatCutDist(ctx.CutDistance, ctx.SheetDiagonal);
writer.WriteLine($"G89 P {lib} ({speedClass} {cutDist})");
var lib = ctx.LibraryFile;
if (!string.IsNullOrEmpty(lib))
{
var speedClass = _speedClassifier.Classify(ctx.CutDistance, ctx.SheetDiagonal);
var cutDist = _speedClassifier.FormatCutDist(ctx.CutDistance, ctx.SheetDiagonal);
writer.WriteLine($"G89 P {lib} ({speedClass} {cutDist})");
}
else
{
writer.WriteLine("(WARNING: No library found)");
}
}
// 4. Pierce and start cut
writer.WriteLine("G84");
// 4. Pierce/beam on — G85 for etch (no pierce), G84 for cut
writer.WriteLine(ctx.IsEtch ? "G85" : "G84");
// 5. Anti-dive off
if (_config.UseAntiDive)
@@ -90,20 +98,20 @@ public sealed class CincinnatiFeatureWriter
{
var sb = new StringBuilder();
// Kerf compensation on first cutting move
if (!kerfEmitted && _config.KerfCompensation == KerfMode.ControllerSide)
// Kerf compensation on first cutting move (skip for etch)
if (!ctx.IsEtch && !kerfEmitted && _config.KerfCompensation == KerfMode.ControllerSide)
{
sb.Append(_config.DefaultKerfSide == KerfSide.Left ? "G41" : "G42");
sb.Append(_config.DefaultKerfSide == KerfSide.Left ? "G41 " : "G42 ");
kerfEmitted = true;
}
sb.Append($"G1X{_fmt.FormatCoord(linear.EndPoint.X)}Y{_fmt.FormatCoord(linear.EndPoint.Y)}");
sb.Append($"G1 X{_fmt.FormatCoord(linear.EndPoint.X)} Y{_fmt.FormatCoord(linear.EndPoint.Y)}");
// Feedrate
var feedVar = GetFeedVariable(linear.Layer);
// Feedrate — etch always uses process feedrate
var feedVar = ctx.IsEtch ? "#148" : GetFeedVariable(linear.Layer);
if (feedVar != lastFeedVar)
{
sb.Append($"F{feedVar}");
sb.Append($" F{feedVar}");
lastFeedVar = feedVar;
}
@@ -114,28 +122,30 @@ public sealed class CincinnatiFeatureWriter
{
var sb = new StringBuilder();
// Kerf compensation on first cutting move
if (!kerfEmitted && _config.KerfCompensation == KerfMode.ControllerSide)
// Kerf compensation on first cutting move (skip for etch)
if (!ctx.IsEtch && !kerfEmitted && _config.KerfCompensation == KerfMode.ControllerSide)
{
sb.Append(_config.DefaultKerfSide == KerfSide.Left ? "G41" : "G42");
sb.Append(_config.DefaultKerfSide == KerfSide.Left ? "G41 " : "G42 ");
kerfEmitted = true;
}
// G2 = CW, G3 = CCW
var gCode = arc.Rotation == RotationType.CW ? "G2" : "G3";
sb.Append($"{gCode}X{_fmt.FormatCoord(arc.EndPoint.X)}Y{_fmt.FormatCoord(arc.EndPoint.Y)}");
sb.Append($"{gCode} X{_fmt.FormatCoord(arc.EndPoint.X)} Y{_fmt.FormatCoord(arc.EndPoint.Y)}");
// Convert absolute center to incremental I/J
var i = arc.CenterPoint.X - currentPos.X;
var j = arc.CenterPoint.Y - currentPos.Y;
sb.Append($"I{_fmt.FormatCoord(i)}J{_fmt.FormatCoord(j)}");
sb.Append($" I{_fmt.FormatCoord(i)} J{_fmt.FormatCoord(j)}");
// Feedrate — full circles use multiplied feedrate
// Feedrate — etch always uses process feedrate, cut uses layer-based
var isFullCircle = IsFullCircle(currentPos, arc.EndPoint);
var feedVar = isFullCircle ? "[#148*#128]" : GetFeedVariable(arc.Layer);
var feedVar = ctx.IsEtch ? "#148"
: isFullCircle ? "[#148*#128]"
: GetFeedVariable(arc.Layer);
if (feedVar != lastFeedVar)
{
sb.Append($"F{feedVar}");
sb.Append($" F{feedVar}");
lastFeedVar = feedVar;
}
@@ -183,9 +193,9 @@ public sealed class CincinnatiFeatureWriter
var sb = new StringBuilder();
if (_config.UseLineNumbers)
sb.Append($"N{featureNumber}");
sb.Append($"N{featureNumber} ");
sb.Append($"G0X{_fmt.FormatCoord(piercePoint.X)}Y{_fmt.FormatCoord(piercePoint.Y)}");
sb.Append($"G0 X{_fmt.FormatCoord(piercePoint.X)} Y{_fmt.FormatCoord(piercePoint.Y)}");
writer.WriteLine(sb.ToString());
}
@@ -194,7 +204,7 @@ public sealed class CincinnatiFeatureWriter
{
if (ctx.IsSafetyHeadraise && _config.SafetyHeadraiseDistance.HasValue)
{
writer.WriteLine($"M47 P{_config.SafetyHeadraiseDistance.Value}(Safety Headraise)");
writer.WriteLine($"M47 P{_config.SafetyHeadraiseDistance.Value} (Safety Headraise)");
return;
}
@@ -27,19 +27,22 @@ public sealed class CincinnatiPartSubprogramWriter
/// The program coordinates must already be normalized to origin (0,0).
/// </summary>
public void Write(TextWriter w, Program normalizedProgram, string drawingName,
int subNumber, string libraryFile, double sheetDiagonal)
int subNumber, string cutLibrary, string etchLibrary, double sheetDiagonal)
{
var features = SplitFeatures(normalizedProgram.Codes);
if (features.Count == 0)
var allFeatures = SplitFeatures(normalizedProgram.Codes);
if (allFeatures.Count == 0)
return;
// Classify and order: etch features first, then cut features
var ordered = OrderFeatures(allFeatures);
w.WriteLine("(*****************************************************)");
w.WriteLine($":{subNumber}");
w.WriteLine(CoordinateFormatter.Comment($"PART: {drawingName}"));
for (var i = 0; i < features.Count; i++)
for (var i = 0; i < ordered.Count; i++)
{
var codes = features[i];
var (codes, isEtch) = ordered[i];
var featureNumber = i == 0
? _config.FeatureLineNumberStart
: 1000 + i + 1;
@@ -51,10 +54,11 @@ public sealed class CincinnatiPartSubprogramWriter
FeatureNumber = featureNumber,
PartName = drawingName,
IsFirstFeatureOfPart = false,
IsLastFeatureOnSheet = i == features.Count - 1,
IsLastFeatureOnSheet = i == ordered.Count - 1,
IsSafetyHeadraise = false,
IsExteriorFeature = false,
LibraryFile = libraryFile,
IsEtch = isEtch,
LibraryFile = isEtch ? etchLibrary : cutLibrary,
CutDistance = cutDistance,
SheetDiagonal = sheetDiagonal
};
@@ -62,8 +66,30 @@ public sealed class CincinnatiPartSubprogramWriter
_featureWriter.Write(w, ctx);
}
w.WriteLine("G0X0Y0");
w.WriteLine($"M99(END OF {drawingName})");
w.WriteLine("G0 X0 Y0");
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>
@@ -1,3 +1,5 @@
using System.Collections.Generic;
namespace OpenNest.Posts.Cincinnati
{
/// <summary>
@@ -153,16 +155,29 @@ namespace OpenNest.Posts.Cincinnati
public G89Mode ProcessParameterMode { get; set; } = G89Mode.LibraryFile;
/// <summary>
/// Gets or sets the default G89 library file path.
/// Default: empty string
/// Gets or sets the default assist gas when Nest.AssistGas is empty.
/// Default: "O2"
/// </summary>
public string DefaultLibraryFile { get; set; } = "";
public string DefaultAssistGas { get; set; } = "O2";
/// <summary>
/// Gets or sets whether to repeat G89 before each feature.
/// Default: true
/// Gets or sets the gas used for etch operations.
/// Independent of the cutting assist gas — etch typically requires a specific gas.
/// Default: "N2"
/// </summary>
public bool RepeatG89BeforeEachFeature { get; set; } = true;
public string DefaultEtchGas { get; set; } = "N2";
/// <summary>
/// Gets or sets the material-to-library mapping for cut operations.
/// Each entry maps (material, thickness, gas) to a G89 library file.
/// </summary>
public List<MaterialLibraryEntry> MaterialLibraries { get; set; } = new();
/// <summary>
/// Gets or sets the gas-to-library mapping for etch operations.
/// Each entry maps a gas type to a G89 etch library file.
/// </summary>
public List<EtchLibraryEntry> EtchLibraries { get; set; } = new();
/// <summary>
/// Gets or sets whether to use exact stop mode (G61).
@@ -272,4 +287,18 @@ namespace OpenNest.Posts.Cincinnati
/// </summary>
public int SheetLengthVariable { get; set; } = 111;
}
public class MaterialLibraryEntry
{
public string Material { get; set; } = "";
public double Thickness { get; set; }
public string Gas { get; set; } = "";
public string Library { get; set; } = "";
}
public class EtchLibraryEntry
{
public string Gas { get; set; } = "";
public string Library { get; set; } = "";
}
}
@@ -68,7 +68,18 @@ namespace OpenNest.Posts.Cincinnati
.Where(p => p.Parts.Count > 0)
.ToList();
// 3. Build part sub-program registry (if enabled)
// 3. Resolve gas and library files
var resolver = new MaterialLibraryResolver(Config);
var gas = MaterialLibraryResolver.ResolveGas(nest, Config);
var etchLibrary = resolver.ResolveEtchLibrary(Config.DefaultEtchGas);
// Resolve cut library from first plate for preamble
var firstPlate = plates.FirstOrDefault();
var initialCutLibrary = firstPlate != null
? resolver.ResolveCutLibrary(firstPlate.Material?.Name ?? "", firstPlate.Thickness, gas)
: "";
// 4. Build part sub-program registry (if enabled)
Dictionary<(int, long), int> partSubprograms = null;
List<(int subNum, string name, Program program)> subprogramEntries = null;
@@ -100,21 +111,21 @@ namespace OpenNest.Posts.Cincinnati
}
}
// 4. Create writers
// 5. Create writers
var preamble = new CincinnatiPreambleWriter(Config);
var sheetWriter = new CincinnatiSheetWriter(Config, vars);
// 5. Build material description from first plate
var material = plates.FirstOrDefault()?.Material;
// 6. Build material description from first plate
var material = firstPlate?.Material;
var materialDesc = material != null
? $"{material.Name}{(string.IsNullOrEmpty(material.Grade) ? "" : $", {material.Grade}")}"
: "";
// 6. Write to stream
// 7. Write to stream
using var writer = new StreamWriter(outputStream, Encoding.UTF8, 1024, leaveOpen: true);
// Main program
preamble.WriteMainProgram(writer, nest.Name ?? "NEST", materialDesc, plates.Count);
preamble.WriteMainProgram(writer, nest.Name ?? "NEST", materialDesc, plates.Count, initialCutLibrary);
// Variable declaration subprogram
preamble.WriteVariableDeclaration(writer, vars);
@@ -122,17 +133,18 @@ namespace OpenNest.Posts.Cincinnati
// Sheet subprograms
for (var i = 0; i < plates.Count; i++)
{
var plate = plates[i];
var sheetIndex = i + 1;
var subNumber = Config.SheetSubprogramStart + i;
sheetWriter.Write(writer, plates[i], nest.Name ?? "NEST", sheetIndex, subNumber,
partSubprograms);
var cutLibrary = resolver.ResolveCutLibrary(plate.Material?.Name ?? "", plate.Thickness, gas);
sheetWriter.Write(writer, plate, nest.Name ?? "NEST", sheetIndex, subNumber,
cutLibrary, etchLibrary, partSubprograms);
}
// Part sub-programs (if enabled)
if (subprogramEntries != null)
{
var partSubWriter = new CincinnatiPartSubprogramWriter(Config);
var firstPlate = plates.FirstOrDefault();
var sheetDiagonal = firstPlate != null
? System.Math.Sqrt(firstPlate.Size.Width * firstPlate.Size.Width
+ firstPlate.Size.Length * firstPlate.Size.Length)
@@ -141,7 +153,7 @@ namespace OpenNest.Posts.Cincinnati
foreach (var (subNum, name, pgm) in subprogramEntries)
{
partSubWriter.Write(writer, pgm, name, subNum,
Config.DefaultLibraryFile ?? "", sheetDiagonal);
initialCutLibrary, etchLibrary, sheetDiagonal);
}
}
@@ -21,7 +21,9 @@ public sealed class CincinnatiPreambleWriter
/// <summary>
/// Writes the main program header block.
/// </summary>
public void WriteMainProgram(TextWriter w, string nestName, string materialDescription, int sheetCount)
/// <param name="initialLibrary">Resolved G89 library file for the initial process setup.</param>
public void WriteMainProgram(TextWriter w, string nestName, string materialDescription,
int sheetCount, string initialLibrary)
{
w.WriteLine(CoordinateFormatter.Comment($"NEST {nestName}"));
w.WriteLine(CoordinateFormatter.Comment($"CONFIGURATION - {_config.ConfigurationName}"));
@@ -39,8 +41,8 @@ public sealed class CincinnatiPreambleWriter
w.WriteLine("M42");
if (_config.ProcessParameterMode == G89Mode.LibraryFile && !string.IsNullOrEmpty(_config.DefaultLibraryFile))
w.WriteLine($"G89 P {_config.DefaultLibraryFile}");
if (_config.ProcessParameterMode == G89Mode.LibraryFile && !string.IsNullOrEmpty(initialLibrary))
w.WriteLine($"G89 P {initialLibrary}");
w.WriteLine($"M98 P{_config.VariableDeclarationSubprogram} (Variable Declaration)");
@@ -49,7 +51,7 @@ public sealed class CincinnatiPreambleWriter
for (var i = 1; i <= sheetCount; i++)
{
var subNum = _config.SheetSubprogramStart + (i - 1);
w.WriteLine($"N{i}M98 P{subNum} (SHEET {i})");
w.WriteLine($"N{i} M98 P{subNum} (SHEET {i})");
}
w.WriteLine("M42");
@@ -30,11 +30,14 @@ public sealed class CincinnatiSheetWriter
/// <summary>
/// Writes a complete sheet subprogram for the given plate.
/// </summary>
/// <param name="cutLibrary">Resolved G89 library file for cut operations.</param>
/// <param name="etchLibrary">Resolved G89 library file for etch operations.</param>
/// <param name="partSubprograms">
/// Optional mapping of (drawingId, rotationKey) to sub-program number.
/// When provided, non-cutoff parts are emitted as M98 calls instead of inline features.
/// </param>
public void Write(TextWriter w, Plate plate, string nestName, int sheetIndex, int subNumber,
string cutLibrary, string etchLibrary,
Dictionary<(int, long), int> partSubprograms = null)
{
if (plate.Parts.Count == 0)
@@ -43,7 +46,6 @@ public sealed class CincinnatiSheetWriter
var width = plate.Size.Width;
var length = plate.Size.Length;
var sheetDiagonal = System.Math.Sqrt(width * width + length * length);
var libraryFile = _config.DefaultLibraryFile ?? "";
var varDeclSub = _config.VariableDeclarationSubprogram;
var partCount = plate.Parts.Count(p => !p.BaseDrawing.IsCutOff);
@@ -55,20 +57,20 @@ public sealed class CincinnatiSheetWriter
w.WriteLine($"( Layout {sheetIndex} )");
w.WriteLine($"( SHEET NAME = {_fmt.FormatCoord(length)} X {_fmt.FormatCoord(width)} )");
w.WriteLine($"( Total parts on sheet = {partCount} )");
w.WriteLine($"#{_config.SheetWidthVariable}={_fmt.FormatCoord(width)}(SHEET WIDTH FOR CUTOFFS)");
w.WriteLine($"#{_config.SheetLengthVariable}={_fmt.FormatCoord(length)}(SHEET LENGTH FOR CUTOFFS)");
w.WriteLine($"#{_config.SheetWidthVariable}={_fmt.FormatCoord(width)} (SHEET WIDTH FOR CUTOFFS)");
w.WriteLine($"#{_config.SheetLengthVariable}={_fmt.FormatCoord(length)} (SHEET LENGTH FOR CUTOFFS)");
// 2. Coordinate setup
w.WriteLine("M42");
w.WriteLine("N10000");
w.WriteLine("G92X#5021Y#5022");
if (!string.IsNullOrEmpty(libraryFile))
w.WriteLine($"G89 P {libraryFile}");
w.WriteLine("G92 X#5021 Y#5022");
if (!string.IsNullOrEmpty(cutLibrary))
w.WriteLine($"G89 P {cutLibrary}");
w.WriteLine($"M98 P{varDeclSub} (Variable Declaration)");
w.WriteLine("G90");
w.WriteLine("M47(CPT)");
if (!string.IsNullOrEmpty(libraryFile))
w.WriteLine($"G89 P {libraryFile}");
w.WriteLine("M47");
if (!string.IsNullOrEmpty(cutLibrary))
w.WriteLine($"G89 P {cutLibrary}");
w.WriteLine("GOTO1( Goto Feature )");
// 3. Order parts: non-cutoff sorted by Bottom then Left, cutoffs last
@@ -86,20 +88,20 @@ public sealed class CincinnatiSheetWriter
// 4. Emit parts
if (partSubprograms != null)
WritePartsWithSubprograms(w, allParts, libraryFile, sheetDiagonal, partSubprograms);
WritePartsWithSubprograms(w, allParts, cutLibrary, etchLibrary, sheetDiagonal, partSubprograms);
else
WritePartsInline(w, allParts, libraryFile, sheetDiagonal);
WritePartsInline(w, allParts, cutLibrary, etchLibrary, sheetDiagonal);
// 5. Footer
w.WriteLine("M42");
w.WriteLine("G0X0Y0");
w.WriteLine("G0 X0 Y0");
if (_config.PalletExchange != PalletMode.None)
w.WriteLine($"N{sheetIndex + 1}M50");
w.WriteLine($"M99(END OF {nestName}.{sheetIndex:D3})");
w.WriteLine($"N{sheetIndex + 1} M50");
w.WriteLine($"M99 (END OF {nestName}.{sheetIndex:D3})");
}
private void WritePartsWithSubprograms(TextWriter w, List<Part> allParts,
string libraryFile, double sheetDiagonal,
string cutLibrary, string etchLibrary, double sheetDiagonal,
Dictionary<(int, long), int> partSubprograms)
{
var lastPartName = "";
@@ -126,26 +128,28 @@ public sealed class CincinnatiSheetWriter
else
{
// Inline features for cutoffs or parts without sub-programs
var features = SplitPartFeatures(part);
var features = SplitAndOrderFeatures(part);
for (var f = 0; f < features.Count; f++)
{
var (codes, isEtch) = features[f];
var featureNumber = featureIndex == 0
? _config.FeatureLineNumberStart
: 1000 + featureIndex + 1;
var isLastFeature = isLastPart && f == features.Count - 1;
var cutDistance = ComputeCutDistance(features[f]);
var cutDistance = ComputeCutDistance(codes);
var ctx = new FeatureContext
{
Codes = features[f],
Codes = codes,
FeatureNumber = featureNumber,
PartName = partName,
IsFirstFeatureOfPart = isNewPart && f == 0,
IsLastFeatureOnSheet = isLastFeature,
IsSafetyHeadraise = isSafetyHeadraise && f == 0,
IsExteriorFeature = false,
LibraryFile = libraryFile,
IsEtch = isEtch,
LibraryFile = isEtch ? etchLibrary : cutLibrary,
CutDistance = cutDistance,
SheetDiagonal = sheetDiagonal
};
@@ -164,7 +168,7 @@ public sealed class CincinnatiSheetWriter
{
// Safety headraise before rapid to new part
if (isSafetyHeadraise && _config.SafetyHeadraiseDistance.HasValue)
w.WriteLine($"M47 P{_config.SafetyHeadraiseDistance.Value}(Safety Headraise)");
w.WriteLine($"M47 P{_config.SafetyHeadraiseDistance.Value} (Safety Headraise)");
// Rapid to part position (bounding box lower-left)
var featureNumber = featureIndex == 0
@@ -173,21 +177,21 @@ public sealed class CincinnatiSheetWriter
var sb = new StringBuilder();
if (_config.UseLineNumbers)
sb.Append($"N{featureNumber}");
sb.Append($"G0X{_fmt.FormatCoord(part.Left)}Y{_fmt.FormatCoord(part.Bottom)}");
sb.Append($"N{featureNumber} ");
sb.Append($"G0 X{_fmt.FormatCoord(part.Left)} Y{_fmt.FormatCoord(part.Bottom)}");
w.WriteLine(sb.ToString());
// Part name comment
w.WriteLine(CoordinateFormatter.Comment($"PART: {partName}"));
// Set local coordinate system at part position
w.WriteLine("G92X0Y0");
w.WriteLine("G92 X0 Y0");
// Call part sub-program
w.WriteLine($"M98P{subNum}({partName})");
w.WriteLine($"M98 P{subNum} ({partName})");
// Restore sheet coordinate system
w.WriteLine($"G92X{_fmt.FormatCoord(part.Left)}Y{_fmt.FormatCoord(part.Bottom)}");
w.WriteLine($"G92 X{_fmt.FormatCoord(part.Left)} Y{_fmt.FormatCoord(part.Bottom)}");
// Head raise (unless last part on sheet)
if (!isLastPart)
@@ -195,36 +199,22 @@ public sealed class CincinnatiSheetWriter
}
private void WritePartsInline(TextWriter w, List<Part> allParts,
string libraryFile, double sheetDiagonal)
string cutLibrary, string etchLibrary, double sheetDiagonal)
{
// Multi-contour splitting
var features = new List<(Part part, List<ICode> codes)>();
// Split and classify features, ordering etch before cut per part
var features = new List<(Part part, List<ICode> codes, bool isEtch)>();
foreach (var part in allParts)
{
List<ICode> current = null;
foreach (var code in part.Program.Codes)
{
if (code is RapidMove)
{
if (current != null)
features.Add((part, current));
current = new List<ICode> { code };
}
else
{
current ??= new List<ICode>();
current.Add(code);
}
}
if (current != null && current.Count > 0)
features.Add((part, current));
var partFeatures = SplitAndOrderFeatures(part);
foreach (var (codes, isEtch) in partFeatures)
features.Add((part, codes, isEtch));
}
// Emit features
var lastPartName = "";
for (var i = 0; i < features.Count; i++)
{
var (part, codes) = features[i];
var (part, codes, isEtch) = features[i];
var partName = part.BaseDrawing.Name;
var isFirstFeatureOfPart = partName != lastPartName;
var isSafetyHeadraise = partName != lastPartName && lastPartName != "";
@@ -245,7 +235,8 @@ public sealed class CincinnatiSheetWriter
IsLastFeatureOnSheet = isLastFeature,
IsSafetyHeadraise = isSafetyHeadraise,
IsExteriorFeature = false,
LibraryFile = libraryFile,
IsEtch = isEtch,
LibraryFile = isEtch ? etchLibrary : cutLibrary,
CutDistance = cutDistance,
SheetDiagonal = sheetDiagonal
};
@@ -255,9 +246,14 @@ public sealed class CincinnatiSheetWriter
}
}
private static List<List<ICode>> SplitPartFeatures(Part part)
/// <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 features = new List<List<ICode>>();
var etchFeatures = new List<List<ICode>>();
var cutFeatures = new List<List<ICode>>();
List<ICode> current = null;
foreach (var code in part.Program.Codes)
@@ -265,7 +261,7 @@ public sealed class CincinnatiSheetWriter
if (code is RapidMove)
{
if (current != null)
features.Add(current);
ClassifyAndAdd(current, etchFeatures, cutFeatures);
current = new List<ICode> { code };
}
else
@@ -276,9 +272,40 @@ public sealed class CincinnatiSheetWriter
}
if (current != null && current.Count > 0)
features.Add(current);
ClassifyAndAdd(current, etchFeatures, cutFeatures);
return features;
// 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)
@@ -0,0 +1,42 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace OpenNest.Posts.Cincinnati;
public sealed class MaterialLibraryResolver
{
private const double ThicknessTolerance = 0.001;
private readonly List<MaterialLibraryEntry> _materialLibraries;
private readonly List<EtchLibraryEntry> _etchLibraries;
public MaterialLibraryResolver(CincinnatiPostConfig config)
{
_materialLibraries = config.MaterialLibraries ?? new List<MaterialLibraryEntry>();
_etchLibraries = config.EtchLibraries ?? new List<EtchLibraryEntry>();
}
public string ResolveCutLibrary(string materialName, double thickness, string gas)
{
var entry = _materialLibraries.FirstOrDefault(e =>
string.Equals(e.Material, materialName, StringComparison.OrdinalIgnoreCase) &&
System.Math.Abs(e.Thickness - thickness) <= ThicknessTolerance &&
string.Equals(e.Gas, gas, StringComparison.OrdinalIgnoreCase));
return entry?.Library ?? "";
}
public string ResolveEtchLibrary(string gas)
{
var entry = _etchLibraries.FirstOrDefault(e =>
string.Equals(e.Gas, gas, StringComparison.OrdinalIgnoreCase));
return entry?.Library ?? "";
}
public static string ResolveGas(Nest nest, CincinnatiPostConfig config)
{
return !string.IsNullOrEmpty(nest.AssistGas) ? nest.AssistGas : config.DefaultAssistGas;
}
}
@@ -13,9 +13,7 @@ public class CincinnatiFeatureWriterTests
UseAntiDive = true,
KerfCompensation = KerfMode.ControllerSide,
DefaultKerfSide = KerfSide.Left,
RepeatG89BeforeEachFeature = true,
ProcessParameterMode = G89Mode.LibraryFile,
DefaultLibraryFile = "MILD10",
InteriorM47 = M47Mode.Always,
ExteriorM47 = M47Mode.Always,
UseSpeedGas = false,
@@ -58,7 +56,7 @@ public class CincinnatiFeatureWriterTests
var output = WriteFeature(config, ctx);
var lines = output.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries);
Assert.StartsWith("N1G0X13.401Y57.4895", lines[0]);
Assert.StartsWith("N1 G0 X13.401 Y57.4895", lines[0]);
}
[Fact]
@@ -70,7 +68,7 @@ public class CincinnatiFeatureWriterTests
var output = WriteFeature(config, ctx);
var lines = output.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries);
Assert.StartsWith("G0X13.401Y57.4895", lines[0]);
Assert.StartsWith("G0 X13.401 Y57.4895", lines[0]);
}
[Fact]
@@ -260,8 +258,8 @@ public class CincinnatiFeatureWriterTests
var cwOutput = WriteFeature(config, SimpleContext(cwCodes));
var ccwOutput = WriteFeature(config, SimpleContext(ccwCodes));
Assert.Contains("G2X", cwOutput);
Assert.Contains("G3X", ccwOutput);
Assert.Contains("G2 X", cwOutput);
Assert.Contains("G3 X", ccwOutput);
}
[Fact]
@@ -289,12 +287,10 @@ public class CincinnatiFeatureWriterTests
}
[Fact]
public void G89_EmittedWhenRepeatEnabled()
public void G89_EmittedWithLibraryFile()
{
var config = DefaultConfig();
config.RepeatG89BeforeEachFeature = true;
config.ProcessParameterMode = G89Mode.LibraryFile;
config.DefaultLibraryFile = "MILD10";
var ctx = SimpleContext();
ctx.LibraryFile = "MILD10";
ctx.CutDistance = 18.0;
@@ -305,14 +301,65 @@ public class CincinnatiFeatureWriterTests
}
[Fact]
public void G89_NotEmittedWhenRepeatDisabled()
public void G89_WarningEmittedWhenNoLibrary()
{
var config = DefaultConfig();
config.RepeatG89BeforeEachFeature = false;
config.ProcessParameterMode = G89Mode.LibraryFile;
var ctx = SimpleContext();
ctx.LibraryFile = "";
var output = WriteFeature(config, ctx);
Assert.DoesNotContain("G89", output);
Assert.Contains("WARNING: No library found", output);
Assert.DoesNotContain("G89 P", output);
}
[Fact]
public void Etch_UsesG85InsteadOfG84()
{
var config = DefaultConfig();
var ctx = SimpleContext();
ctx.IsEtch = true;
ctx.LibraryFile = "EtchN2.lib";
var output = WriteFeature(config, ctx);
Assert.Contains("G85", output);
Assert.DoesNotContain("G84", output);
}
[Fact]
public void Etch_SkipsKerfCompensation()
{
var config = DefaultConfig();
config.KerfCompensation = KerfMode.ControllerSide;
var ctx = SimpleContext();
ctx.IsEtch = true;
ctx.LibraryFile = "EtchN2.lib";
var output = WriteFeature(config, ctx);
Assert.DoesNotContain("G41", output);
Assert.DoesNotContain("G42", output);
Assert.DoesNotContain("G40", output);
}
[Fact]
public void Etch_AllMovesUseProcessFeedrate()
{
var config = DefaultConfig();
config.KerfCompensation = KerfMode.PreApplied;
var codes = new List<ICode>
{
new RapidMove(1.0, 1.0),
new LinearMove(2.0, 1.0) { Layer = LayerType.Leadin },
new LinearMove(3.0, 1.0) { Layer = LayerType.Cut }
};
var ctx = SimpleContext(codes);
ctx.IsEtch = true;
ctx.LibraryFile = "EtchN2.lib";
var output = WriteFeature(config, ctx);
// Should use #148 for all moves, not #126 for lead-in
Assert.DoesNotContain("F#126", output);
Assert.Contains("F#148", output);
}
[Fact]
@@ -378,7 +425,7 @@ public class CincinnatiFeatureWriterTests
ctx.IsLastFeatureOnSheet = false;
var output = WriteFeature(config, ctx);
Assert.Contains("M47 P2000(Safety Headraise)", output);
Assert.Contains("M47 P2000 (Safety Headraise)", output);
}
[Fact]
@@ -404,7 +451,7 @@ public class CincinnatiFeatureWriterTests
var lines = output.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries);
// Find indices of key lines
var rapidIdx = Array.FindIndex(lines, l => l.Contains("G0X"));
var rapidIdx = Array.FindIndex(lines, l => l.Contains("G0 X"));
var partIdx = Array.FindIndex(lines, l => l.Contains("PART:"));
var g89Idx = Array.FindIndex(lines, l => l.Contains("G89"));
var g84Idx = Array.FindIndex(lines, l => l.Contains("G84"));
@@ -17,7 +17,6 @@ public class CincinnatiPostProcessorTests
var config = new CincinnatiPostConfig
{
ConfigurationName = "CL940",
DefaultLibraryFile = "MS135N2PANEL.lib",
PostedAccuracy = 4
};
var post = new CincinnatiPostProcessor(config);
@@ -72,7 +71,7 @@ public class CincinnatiPostProcessorTests
var output = Encoding.UTF8.GetString(ms.ToArray());
// Should only have one sheet subprogram call in main
Assert.Contains("N1M98 P101 (SHEET 1)", output);
Assert.Contains("N1 M98 P101 (SHEET 1)", output);
Assert.DoesNotContain("SHEET 2", output);
}
@@ -104,10 +103,19 @@ public class CincinnatiPostProcessorTests
var config = new CincinnatiPostConfig
{
ConfigurationName = "CL940_CORONA",
DefaultLibraryFile = "MS135N2PANEL.lib",
DefaultAssistGas = "N2",
DefaultEtchGas = "N2",
PostedUnits = Units.Inches,
KerfCompensation = KerfMode.ControllerSide,
UseAntiDive = true
UseAntiDive = true,
MaterialLibraries = new()
{
new MaterialLibraryEntry { Material = "Mild Steel", Thickness = 0.135, Gas = "N2", Library = "MS135N2PANEL.lib" }
},
EtchLibraries = new()
{
new EtchLibraryEntry { Gas = "N2", Library = "EtchN2.lib" }
}
};
var opts = new JsonSerializerOptions
@@ -119,10 +127,15 @@ public class CincinnatiPostProcessorTests
var deserialized = JsonSerializer.Deserialize<CincinnatiPostConfig>(json, opts);
Assert.Equal("CL940_CORONA", deserialized.ConfigurationName);
Assert.Equal("MS135N2PANEL.lib", deserialized.DefaultLibraryFile);
Assert.Equal("N2", deserialized.DefaultAssistGas);
Assert.Equal("N2", deserialized.DefaultEtchGas);
Assert.Equal(Units.Inches, deserialized.PostedUnits);
Assert.Equal(KerfMode.ControllerSide, deserialized.KerfCompensation);
Assert.True(deserialized.UseAntiDive);
Assert.Single(deserialized.MaterialLibraries);
Assert.Equal("MS135N2PANEL.lib", deserialized.MaterialLibraries[0].Library);
Assert.Single(deserialized.EtchLibraries);
Assert.Equal("EtchN2.lib", deserialized.EtchLibraries[0].Library);
// Enums serialize as strings
Assert.Contains("\"Inches\"", json);
@@ -157,21 +170,21 @@ public class CincinnatiPostProcessorTests
var output = Encoding.UTF8.GetString(ms.ToArray());
// Sheet should contain M98 call to part sub-program
Assert.Contains("M98P200", output);
Assert.Contains("M98 P200", output);
// Should have G92 for local coordinate positioning
Assert.Contains("G92X0Y0", output);
Assert.Contains("G92 X0 Y0", output);
// Part sub-program definition
Assert.Contains(":200", output);
Assert.Contains("G84", output);
// Sub-program ends with G0X0Y0 and M99
Assert.Contains("G0X0Y0", output);
Assert.Contains("M99(END OF Square)", output);
// Sub-program ends with G0 X0 Y0 and M99
Assert.Contains("G0 X0 Y0", output);
Assert.Contains("M99 (END OF Square)", output);
// G92 restore after M98 call
Assert.Contains("G92X", output);
Assert.Contains("G92 X", output);
}
[Fact]
@@ -198,7 +211,7 @@ public class CincinnatiPostProcessorTests
var output = Encoding.UTF8.GetString(ms.ToArray());
// Both parts should call the same sub-program
var m98Count = System.Text.RegularExpressions.Regex.Matches(output, "M98P200").Count;
var m98Count = System.Text.RegularExpressions.Regex.Matches(output, @"M98 P200\b").Count;
Assert.Equal(2, m98Count);
// Only one sub-program definition
@@ -238,8 +251,8 @@ public class CincinnatiPostProcessorTests
// Should have two different sub-programs
Assert.Contains(":200", output);
Assert.Contains(":201", output);
Assert.Contains("M98P200", output);
Assert.Contains("M98P201", output);
Assert.Contains("M98 P200", output);
Assert.Contains("M98 P201", output);
}
[Fact]
@@ -268,7 +281,7 @@ public class CincinnatiPostProcessorTests
var output = Encoding.UTF8.GetString(ms.ToArray());
// Regular part uses sub-program
Assert.Contains("M98P200", output);
Assert.Contains("M98 P200", output);
Assert.Contains(":200", output);
// Cutoff should NOT have its own sub-program
@@ -13,14 +13,13 @@ public class CincinnatiPreambleWriterTests
var config = new CincinnatiPostConfig
{
ConfigurationName = "CL940",
PostedUnits = Units.Inches,
DefaultLibraryFile = "MS135N2PANEL.lib"
PostedUnits = Units.Inches
};
var sb = new StringBuilder();
using var sw = new StringWriter(sb);
var writer = new CincinnatiPreambleWriter(config);
writer.WriteMainProgram(sw, "TestNest", "Mild Steel, 10GA", 2);
writer.WriteMainProgram(sw, "TestNest", "Mild Steel, 10GA", 2, "MS135N2PANEL.lib");
var output = sb.ToString();
Assert.Contains("( NEST TestNest )", output);
@@ -30,8 +29,8 @@ public class CincinnatiPreambleWriterTests
Assert.Contains("G89 P MS135N2PANEL.lib", output);
Assert.Contains("M98 P100 (Variable Declaration)", output);
Assert.Contains("GOTO1 (GOTO SHEET NUMBER)", output);
Assert.Contains("N1M98 P101 (SHEET 1)", output);
Assert.Contains("N2M98 P102 (SHEET 2)", output);
Assert.Contains("N1 M98 P101 (SHEET 1)", output);
Assert.Contains("N2 M98 P102 (SHEET 2)", output);
Assert.Contains("M30 (END OF MAIN)", output);
}
@@ -43,7 +42,7 @@ public class CincinnatiPreambleWriterTests
using var sw = new StringWriter(sb);
var writer = new CincinnatiPreambleWriter(config);
writer.WriteMainProgram(sw, "Test", "", 1);
writer.WriteMainProgram(sw, "Test", "", 1, "");
Assert.Contains("G21", sb.ToString());
}
@@ -56,7 +55,7 @@ public class CincinnatiPreambleWriterTests
using var sw = new StringWriter(sb);
var writer = new CincinnatiPreambleWriter(config);
writer.WriteMainProgram(sw, "Test", "", 1);
writer.WriteMainProgram(sw, "Test", "", 1, "");
Assert.Contains("G61", sb.ToString());
}
@@ -69,7 +68,7 @@ public class CincinnatiPreambleWriterTests
using var sw = new StringWriter(sb);
var writer = new CincinnatiPreambleWriter(config);
writer.WriteMainProgram(sw, "Test", "", 1);
writer.WriteMainProgram(sw, "Test", "", 1, "");
Assert.DoesNotContain("G61", sb.ToString());
}
@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
@@ -14,7 +15,6 @@ public class CincinnatiSheetWriterTests
{
var config = new CincinnatiPostConfig
{
DefaultLibraryFile = "MS135N2PANEL.lib",
PostedAccuracy = 4
};
var plate = new Plate(48.0, 96.0);
@@ -24,14 +24,15 @@ public class CincinnatiSheetWriterTests
using var sw = new StringWriter(sb);
var sheetWriter = new CincinnatiSheetWriter(config, new ProgramVariableManager());
sheetWriter.Write(sw, plate, "TestNest", 1, 101);
sheetWriter.Write(sw, plate, "TestNest", 1, 101, "MS135N2PANEL.lib", "EtchN2.lib");
var output = sb.ToString();
Assert.Contains(":101", output);
Assert.Contains("( Sheet 1 )", output);
Assert.Contains("#110=", output);
Assert.Contains("#111=", output);
Assert.Contains("G92X#5021Y#5022", output);
Assert.Contains("G92 X#5021 Y#5022", output);
Assert.Contains("G89 P MS135N2PANEL.lib", output);
Assert.Contains("M99", output);
}
@@ -50,11 +51,11 @@ public class CincinnatiSheetWriterTests
using var sw = new StringWriter(sb);
var sheetWriter = new CincinnatiSheetWriter(config, new ProgramVariableManager());
sheetWriter.Write(sw, plate, "TestNest", 1, 101);
sheetWriter.Write(sw, plate, "TestNest", 1, 101, "", "");
var output = sb.ToString();
Assert.Contains("M42", output);
Assert.Contains("G0X0Y0", output);
Assert.Contains("G0 X0 Y0", output);
Assert.Contains("M50", output);
}
@@ -68,7 +69,7 @@ public class CincinnatiSheetWriterTests
using var sw = new StringWriter(sb);
var sheetWriter = new CincinnatiSheetWriter(config, new ProgramVariableManager());
sheetWriter.Write(sw, plate, "TestNest", 1, 101);
sheetWriter.Write(sw, plate, "TestNest", 1, 101, "", "");
Assert.Equal("", sb.ToString());
}
@@ -96,7 +97,7 @@ public class CincinnatiSheetWriterTests
using var sw = new StringWriter(sb);
var sheetWriter = new CincinnatiSheetWriter(config, new ProgramVariableManager());
sheetWriter.Write(sw, plate, "TestNest", 1, 101);
sheetWriter.Write(sw, plate, "TestNest", 1, 101, "", "");
var output = sb.ToString();
// Should have two G84 pierce commands (one per contour)
@@ -104,6 +105,80 @@ public class CincinnatiSheetWriterTests
Assert.Equal(2, g84Count);
}
[Fact]
public void WriteSheet_EtchFeaturesOrderedBeforeCut()
{
var config = new CincinnatiPostConfig { PostedAccuracy = 4 };
var pgm = new Program();
// Cut contour first in program
pgm.Codes.Add(new RapidMove(0, 0));
pgm.Codes.Add(new LinearMove(5, 0) { Layer = LayerType.Cut });
pgm.Codes.Add(new LinearMove(5, 5) { Layer = LayerType.Cut });
// Etch contour second in program
pgm.Codes.Add(new RapidMove(1, 1));
pgm.Codes.Add(new LinearMove(2, 1) { Layer = LayerType.Scribe });
pgm.Codes.Add(new LinearMove(2, 2) { Layer = LayerType.Scribe });
var plate = new Plate(48.0, 96.0);
plate.Parts.Add(new Part(new Drawing("MixedPart", pgm)));
var sb = new StringBuilder();
using var sw = new StringWriter(sb);
var sheetWriter = new CincinnatiSheetWriter(config, new ProgramVariableManager());
sheetWriter.Write(sw, plate, "TestNest", 1, 101, "MS250O2.lib", "EtchN2.lib");
var output = sb.ToString();
// Etch (G85) should appear before cut (G84)
var g85Idx = output.IndexOf("G85");
var g84Idx = output.IndexOf("G84");
Assert.True(g85Idx >= 0, "G85 should be present for etch");
Assert.True(g84Idx >= 0, "G84 should be present for cut");
Assert.True(g85Idx < g84Idx, "G85 (etch) should come before G84 (cut)");
// Etch uses etch library
Assert.Contains("G89 P EtchN2.lib", output);
// Cut uses cut library
Assert.Contains("G89 P MS250O2.lib", output);
}
[Fact]
public void IsFeatureEtch_ReturnsTrueForScribeLayer()
{
var codes = new List<ICode>
{
new RapidMove(0, 0),
new LinearMove(1, 0) { Layer = LayerType.Scribe },
new LinearMove(1, 1) { Layer = LayerType.Scribe }
};
Assert.True(CincinnatiSheetWriter.IsFeatureEtch(codes));
}
[Fact]
public void IsFeatureEtch_ReturnsFalseForCutLayer()
{
var codes = new List<ICode>
{
new RapidMove(0, 0),
new LinearMove(1, 0) { Layer = LayerType.Cut },
new LinearMove(1, 1) { Layer = LayerType.Cut }
};
Assert.False(CincinnatiSheetWriter.IsFeatureEtch(codes));
}
[Fact]
public void IsFeatureEtch_ReturnsFalseForRapidsOnly()
{
var codes = new List<ICode>
{
new RapidMove(0, 0)
};
Assert.False(CincinnatiSheetWriter.IsFeatureEtch(codes));
}
private static Program CreateSimpleProgram()
{
var pgm = new Program();
@@ -0,0 +1,160 @@
using OpenNest.Posts.Cincinnati;
namespace OpenNest.Tests.Cincinnati;
public class MaterialLibraryResolverTests
{
private static CincinnatiPostConfig ConfigWithLibraries() => new()
{
DefaultAssistGas = "O2",
DefaultEtchGas = "N2",
MaterialLibraries = new()
{
new MaterialLibraryEntry { Material = "Mild Steel", Thickness = 0.250, Gas = "O2", Library = "MS250O2.lib" },
new MaterialLibraryEntry { Material = "Mild Steel", Thickness = 0.250, Gas = "N2", Library = "MS250N2.lib" },
new MaterialLibraryEntry { Material = "Aluminum", Thickness = 0.125, Gas = "N2", Library = "AL125N2.lib" },
new MaterialLibraryEntry { Material = "Stainless Steel", Thickness = 0.375, Gas = "AIR", Library = "SS375AIR.lib" }
},
EtchLibraries = new()
{
new EtchLibraryEntry { Gas = "N2", Library = "EtchN2.lib" },
new EtchLibraryEntry { Gas = "O2", Library = "EtchO2.lib" },
new EtchLibraryEntry { Gas = "AIR", Library = "EtchAIR.lib" }
}
};
[Fact]
public void ResolveCutLibrary_ExactMatch()
{
var resolver = new MaterialLibraryResolver(ConfigWithLibraries());
var result = resolver.ResolveCutLibrary("Mild Steel", 0.250, "O2");
Assert.Equal("MS250O2.lib", result);
}
[Fact]
public void ResolveCutLibrary_CaseInsensitiveMaterial()
{
var resolver = new MaterialLibraryResolver(ConfigWithLibraries());
var result = resolver.ResolveCutLibrary("mild steel", 0.250, "O2");
Assert.Equal("MS250O2.lib", result);
}
[Fact]
public void ResolveCutLibrary_CaseInsensitiveGas()
{
var resolver = new MaterialLibraryResolver(ConfigWithLibraries());
var result = resolver.ResolveCutLibrary("Mild Steel", 0.250, "o2");
Assert.Equal("MS250O2.lib", result);
}
[Fact]
public void ResolveCutLibrary_ThicknessWithinTolerance()
{
var resolver = new MaterialLibraryResolver(ConfigWithLibraries());
var result = resolver.ResolveCutLibrary("Mild Steel", 0.2505, "O2");
Assert.Equal("MS250O2.lib", result);
}
[Fact]
public void ResolveCutLibrary_ThicknessOutsideTolerance_ReturnsEmpty()
{
var resolver = new MaterialLibraryResolver(ConfigWithLibraries());
var result = resolver.ResolveCutLibrary("Mild Steel", 0.260, "O2");
Assert.Equal("", result);
}
[Fact]
public void ResolveCutLibrary_NoMatch_ReturnsEmpty()
{
var resolver = new MaterialLibraryResolver(ConfigWithLibraries());
var result = resolver.ResolveCutLibrary("Titanium", 0.250, "O2");
Assert.Equal("", result);
}
[Fact]
public void ResolveCutLibrary_WrongGas_ReturnsEmpty()
{
var resolver = new MaterialLibraryResolver(ConfigWithLibraries());
var result = resolver.ResolveCutLibrary("Mild Steel", 0.250, "AIR");
Assert.Equal("", result);
}
[Fact]
public void ResolveCutLibrary_DifferentGasSameMaterial()
{
var resolver = new MaterialLibraryResolver(ConfigWithLibraries());
var o2 = resolver.ResolveCutLibrary("Mild Steel", 0.250, "O2");
var n2 = resolver.ResolveCutLibrary("Mild Steel", 0.250, "N2");
Assert.Equal("MS250O2.lib", o2);
Assert.Equal("MS250N2.lib", n2);
}
[Fact]
public void ResolveCutLibrary_EmptyList_ReturnsEmpty()
{
var config = new CincinnatiPostConfig { MaterialLibraries = new() };
var resolver = new MaterialLibraryResolver(config);
var result = resolver.ResolveCutLibrary("Mild Steel", 0.250, "O2");
Assert.Equal("", result);
}
[Fact]
public void ResolveEtchLibrary_ExactMatch()
{
var resolver = new MaterialLibraryResolver(ConfigWithLibraries());
var result = resolver.ResolveEtchLibrary("N2");
Assert.Equal("EtchN2.lib", result);
}
[Fact]
public void ResolveEtchLibrary_CaseInsensitive()
{
var resolver = new MaterialLibraryResolver(ConfigWithLibraries());
var result = resolver.ResolveEtchLibrary("n2");
Assert.Equal("EtchN2.lib", result);
}
[Fact]
public void ResolveEtchLibrary_NoMatch_ReturnsEmpty()
{
var resolver = new MaterialLibraryResolver(ConfigWithLibraries());
var result = resolver.ResolveEtchLibrary("Argon");
Assert.Equal("", result);
}
[Fact]
public void ResolveEtchLibrary_EmptyList_ReturnsEmpty()
{
var config = new CincinnatiPostConfig { EtchLibraries = new() };
var resolver = new MaterialLibraryResolver(config);
var result = resolver.ResolveEtchLibrary("N2");
Assert.Equal("", result);
}
[Fact]
public void ResolveGas_UsesNestAssistGas_WhenSet()
{
var nest = new Nest("Test") { AssistGas = "N2" };
var config = new CincinnatiPostConfig { DefaultAssistGas = "O2" };
var result = MaterialLibraryResolver.ResolveGas(nest, config);
Assert.Equal("N2", result);
}
[Fact]
public void ResolveGas_FallsBackToConfig_WhenNestEmpty()
{
var nest = new Nest("Test") { AssistGas = "" };
var config = new CincinnatiPostConfig { DefaultAssistGas = "O2" };
var result = MaterialLibraryResolver.ResolveGas(nest, config);
Assert.Equal("O2", result);
}
[Fact]
public void ResolveGas_FallsBackToConfig_WhenNestNull()
{
var nest = new Nest("Test");
var config = new CincinnatiPostConfig { DefaultAssistGas = "AIR" };
var result = MaterialLibraryResolver.ResolveGas(nest, config);
Assert.Equal("AIR", result);
}
}
+10 -10
View File
@@ -154,10 +154,10 @@ public class CutOffTests
// Plate(100, 50) = Width=100, Length=50. Vertical cut runs along Y (Width axis).
// BoundingBox Y extent = Size.Width = 100. With 2" overtravel = 102.
// Default TowardOrigin: RapidMove to far end (102), LinearMove to near end (0).
var rapidMoves = cutoff.Drawing.Program.Codes.OfType<RapidMove>().ToList();
Assert.Single(rapidMoves);
Assert.Equal(102.0, rapidMoves[0].EndPoint.Y, 5);
// Default AwayFromOrigin: RapidMove to near end (0), LinearMove to far end (102).
var linearMoves = cutoff.Drawing.Program.Codes.OfType<LinearMove>().ToList();
Assert.Single(linearMoves);
Assert.Equal(102.0, linearMoves[0].EndPoint.Y, 5);
}
[Fact]
@@ -171,11 +171,10 @@ public class CutOffTests
};
cutoff.Regenerate(plate, settings);
// AwayFromOrigin: RapidMove to near end (StartLimit=20), LinearMove to far end (100).
var rapidMoves = cutoff.Drawing.Program.Codes.OfType<RapidMove>().ToList();
Assert.Single(rapidMoves);
var linearMoves = cutoff.Drawing.Program.Codes.OfType<LinearMove>().ToList();
Assert.Single(linearMoves);
Assert.Equal(20.0, linearMoves[0].EndPoint.Y, 5);
Assert.Equal(20.0, rapidMoves[0].EndPoint.Y, 5);
}
[Fact]
@@ -189,9 +188,10 @@ public class CutOffTests
};
cutoff.Regenerate(plate, settings);
var rapidMoves = cutoff.Drawing.Program.Codes.OfType<RapidMove>().ToList();
Assert.Single(rapidMoves);
Assert.Equal(80.0, rapidMoves[0].EndPoint.Y, 5);
// AwayFromOrigin: RapidMove to near end (0), LinearMove to far end (EndLimit=80).
var linearMoves = cutoff.Drawing.Program.Codes.OfType<LinearMove>().ToList();
Assert.Single(linearMoves);
Assert.Equal(80.0, linearMoves[0].EndPoint.Y, 5);
}
[Fact]
+41
View File
@@ -36,6 +36,47 @@ public class PolygonHelperTests
$"With-spacing width: {withSpacing.Polygon.BoundingBox.Width:F3}");
}
[Fact]
public void ExtractPerimeterPolygon_InflatedPolygonIsLarger_ForCWWinding()
{
// CW winding (standard CNC convention): (0,0)→(0,10)→(10,10)→(10,0)→(0,0)
var drawing = TestHelpers.MakeSquareDrawing(10);
var noSpacing = PolygonHelper.ExtractPerimeterPolygon(drawing, 0);
var withSpacing = PolygonHelper.ExtractPerimeterPolygon(drawing, 1);
noSpacing.Polygon.UpdateBounds();
withSpacing.Polygon.UpdateBounds();
Assert.True(withSpacing.Polygon.BoundingBox.Width > noSpacing.Polygon.BoundingBox.Width,
$"Inflated width {withSpacing.Polygon.BoundingBox.Width:F3} should be > original {noSpacing.Polygon.BoundingBox.Width:F3}");
Assert.True(withSpacing.Polygon.BoundingBox.Length > noSpacing.Polygon.BoundingBox.Length,
$"Inflated length {withSpacing.Polygon.BoundingBox.Length:F3} should be > original {noSpacing.Polygon.BoundingBox.Length:F3}");
}
[Fact]
public void ExtractPerimeterPolygon_InflatedPolygonIsLarger_ForCCWWinding()
{
// CCW winding: (0,0)→(10,0)→(10,10)→(0,10)→(0,0)
var pgm = new CNC.Program();
pgm.Codes.Add(new CNC.RapidMove(new Vector(0, 0)));
pgm.Codes.Add(new CNC.LinearMove(new Vector(10, 0)));
pgm.Codes.Add(new CNC.LinearMove(new Vector(10, 10)));
pgm.Codes.Add(new CNC.LinearMove(new Vector(0, 10)));
pgm.Codes.Add(new CNC.LinearMove(new Vector(0, 0)));
var drawing = new Drawing("ccw-square", pgm);
var noSpacing = PolygonHelper.ExtractPerimeterPolygon(drawing, 0);
var withSpacing = PolygonHelper.ExtractPerimeterPolygon(drawing, 1);
noSpacing.Polygon.UpdateBounds();
withSpacing.Polygon.UpdateBounds();
Assert.True(withSpacing.Polygon.BoundingBox.Width > noSpacing.Polygon.BoundingBox.Width,
$"Inflated width {withSpacing.Polygon.BoundingBox.Width:F3} should be > original {noSpacing.Polygon.BoundingBox.Width:F3}");
Assert.True(withSpacing.Polygon.BoundingBox.Length > noSpacing.Polygon.BoundingBox.Length,
$"Inflated length {withSpacing.Polygon.BoundingBox.Length:F3} should be > original {noSpacing.Polygon.BoundingBox.Length:F3}");
}
[Fact]
public void ExtractPerimeterPolygon_ReturnsNull_ForEmptyDrawing()
{
+18
View File
@@ -187,7 +187,25 @@ namespace OpenNest.Actions
var boxes = new List<Box>();
foreach (var part in plate.Parts)
{
if (part.BaseDrawing.IsCutOff)
continue;
boxes.Add(part.BoundingBox.Offset(plate.PartSpacing));
}
var plateBounds = plate.BoundingBox(includeParts: false);
foreach (var cutoff in plate.CutOffs)
{
Box cutoffBox;
if (cutoff.Axis == CutOffAxis.Vertical)
cutoffBox = new Box(cutoff.Position.X, plateBounds.Y, 0, plateBounds.Length);
else
cutoffBox = new Box(plateBounds.X, cutoff.Position.Y, plateBounds.Width, 0);
boxes.Add(cutoffBox.Offset(plate.PartSpacing));
}
var pt = plateView.CurrentPoint;
var vertical = SpatialQuery.GetLargestBoxVertically(pt, bounds, boxes);
+24
View File
@@ -157,7 +157,31 @@ namespace OpenNest.Actions
public void Update()
{
foreach (var part in plateView.Plate.Parts)
{
if (part.BaseDrawing.IsCutOff)
continue;
boxes.Add(part.BoundingBox.Offset(plateView.Plate.PartSpacing));
}
// Add thin obstacle boxes from cutoff definitions so that
// the area selection correctly treats cutoffs as boundaries.
// Cutoff Parts have inflated bounding boxes (their programs use
// absolute coordinates, causing BoundingBox to span from origin)
// so we derive the position directly from the CutOff definition.
var plateBounds = plateView.Plate.BoundingBox(includeParts: false);
foreach (var cutoff in plateView.Plate.CutOffs)
{
Box cutoffBox;
if (cutoff.Axis == CutOffAxis.Vertical)
cutoffBox = new Box(cutoff.Position.X, plateBounds.Y, 0, plateBounds.Length);
else
cutoffBox = new Box(plateBounds.X, cutoff.Position.Y, plateBounds.Width, 0);
boxes.Add(cutoffBox.Offset(plateView.Plate.PartSpacing));
}
Bounds = plateView.Plate.WorkArea();
}
+1
View File
@@ -458,6 +458,7 @@ namespace OpenNest.Forms
PlateView.ZoomToPlate();
PlateView.Refresh();
UpdatePlateList();
UpdatePlateHeader();
}
public void SelectAllParts()
+26 -3
View File
@@ -149,6 +149,8 @@
engineComboBox = new System.Windows.Forms.ToolStripComboBox();
btnAutoNest = new System.Windows.Forms.ToolStripButton();
btnShowRemnants = new System.Windows.Forms.ToolStripButton();
toolStripSeparator5 = new System.Windows.Forms.ToolStripSeparator();
btnCutOff = new System.Windows.Forms.ToolStripButton();
pEPToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
openNestToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
menuStrip1.SuspendLayout();
@@ -917,7 +919,7 @@
// toolStrip1
//
toolStrip1.AutoSize = false;
toolStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { btnNew, btnOpen, btnSave, btnSaveAs, toolStripSeparator1, btnZoomOut, btnZoomIn, btnZoomToFit, toolStripSeparator4, engineLabel, engineComboBox, btnAutoNest, btnShowRemnants });
toolStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { btnNew, btnOpen, btnSave, btnSaveAs, toolStripSeparator1, btnZoomOut, btnZoomIn, btnZoomToFit, toolStripSeparator4, engineLabel, engineComboBox, btnAutoNest, btnShowRemnants, toolStripSeparator5, btnCutOff });
toolStrip1.Location = new System.Drawing.Point(0, 24);
toolStrip1.Name = "toolStrip1";
toolStrip1.Size = new System.Drawing.Size(1281, 40);
@@ -1044,12 +1046,31 @@
//
// btnShowRemnants
//
btnShowRemnants.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
btnShowRemnants.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image;
btnShowRemnants.Image = Properties.Resources.remnants;
btnShowRemnants.ImageScaling = System.Windows.Forms.ToolStripItemImageScaling.None;
btnShowRemnants.Name = "btnShowRemnants";
btnShowRemnants.Size = new System.Drawing.Size(64, 37);
btnShowRemnants.Padding = new System.Windows.Forms.Padding(5, 0, 5, 0);
btnShowRemnants.Size = new System.Drawing.Size(38, 37);
btnShowRemnants.Text = "Remnants";
btnShowRemnants.Click += ShowRemnants_Click;
//
// toolStripSeparator5
//
toolStripSeparator5.Name = "toolStripSeparator5";
toolStripSeparator5.Size = new System.Drawing.Size(6, 40);
//
// btnCutOff
//
btnCutOff.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image;
btnCutOff.Image = Properties.Resources.cutoff;
btnCutOff.ImageScaling = System.Windows.Forms.ToolStripItemImageScaling.None;
btnCutOff.Name = "btnCutOff";
btnCutOff.Padding = new System.Windows.Forms.Padding(5, 0, 5, 0);
btnCutOff.Size = new System.Drawing.Size(38, 37);
btnCutOff.Text = "Sheet Cut-Off";
btnCutOff.Click += CutOff_Click;
//
// pEPToolStripMenuItem
//
pEPToolStripMenuItem.Name = "pEPToolStripMenuItem";
@@ -1213,6 +1234,8 @@
private System.Windows.Forms.ToolStripComboBox engineComboBox;
private System.Windows.Forms.ToolStripButton btnAutoNest;
private System.Windows.Forms.ToolStripButton btnShowRemnants;
private System.Windows.Forms.ToolStripSeparator toolStripSeparator5;
private System.Windows.Forms.ToolStripButton btnCutOff;
private System.Windows.Forms.ToolStripMenuItem mnuPlateCutOff;
}
}
+20
View File
@@ -80,6 +80,16 @@ namespace OpenNest.Properties {
}
}
/// <summary>
/// Looks up a localized resource of type System.Drawing.Bitmap.
/// </summary>
internal static System.Drawing.Bitmap cutoff {
get {
object obj = ResourceManager.GetObject("cutoff", resourceCulture);
return ((System.Drawing.Bitmap)(obj));
}
}
/// <summary>
/// Looks up a localized resource of type System.Drawing.Bitmap.
/// </summary>
@@ -160,6 +170,16 @@ namespace OpenNest.Properties {
}
}
/// <summary>
/// Looks up a localized resource of type System.Drawing.Bitmap.
/// </summary>
internal static System.Drawing.Bitmap remnants {
get {
object obj = ResourceManager.GetObject("remnants", resourceCulture);
return ((System.Drawing.Bitmap)(obj));
}
}
/// <summary>
/// Looks up a localized resource of type System.Drawing.Bitmap.
/// </summary>
+6
View File
@@ -175,4 +175,10 @@
<data name="zoom_out" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\zoom_out.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="cutoff" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\cutoff.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="remnants" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\remnants.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
</root>
Binary file not shown.

After

Width:  |  Height:  |  Size: 994 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

+10
View File
@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\OpenNest.Core\OpenNest.Core.csproj" />
<ProjectReference Include="..\..\OpenNest.IO\OpenNest.IO.csproj" />
</ItemGroup>
</Project>
+209
View File
@@ -0,0 +1,209 @@
using System;
using System.Drawing;
using System.IO;
using System.Linq;
using OpenNest;
using OpenNest.Converters;
using OpenNest.Geometry;
using Size = OpenNest.Geometry.Size;
using OpenNest.IO;
var partColors = new Color[]
{
Color.FromArgb(205, 92, 92), // Indian Red
Color.FromArgb(148, 103, 189), // Medium Purple
Color.FromArgb(75, 180, 175), // Teal
Color.FromArgb(210, 190, 75), // Goldenrod
Color.FromArgb(190, 85, 175), // Orchid
Color.FromArgb(185, 115, 85), // Sienna
Color.FromArgb(120, 100, 190), // Slate Blue
Color.FromArgb(200, 100, 140), // Rose
Color.FromArgb(80, 175, 155), // Sea Green
Color.FromArgb(195, 160, 85), // Dark Khaki
Color.FromArgb(175, 95, 160), // Plum
Color.FromArgb(215, 130, 130), // Light Coral
};
var templateDir = @"C:\Users\AJ\Desktop\Projects\OpenNest\docs\Templates";
var outputDir = @"C:\Users\AJ\Desktop\Projects\OpenNest\docs\Templates\Nests";
Directory.CreateDirectory(outputDir);
// BOM: (fileName, qty, thickness, material)
var bom = new (string File, int Qty, double Thickness, string Material)[]
{
("PT01", 2, 0.250, "304SS"),
("PT02", 5, 0.625, "304SS"),
("PT03", 4, 0.250, "304SS"),
("PT04", 4, 0.250, "304SS"),
("PT05", 1, 0.250, "304SS"),
("PT06", 21, 0.375, "304SS"),
("PT07", 2, 0.250, "304SS"),
("PT08", 22, 0.250, "304SS"),
("PT11", 2, 0.1875, "304SS"),
("PT12", 2, 0.1875, "304SS"),
("PT13", 6, 0.1875, "304SS"),
("PT15", 6, 0.1875, "304SS"),
("PT16", 6, 0.1875, "304SS"),
("PT18", 3, 0.250, "304SS"),
("PT19", 3, 0.1875, "304SS"),
("PT20", 2, 0.1196, "304SS"),
("PT21", 6, 0.1196, "304SS"),
("PT22", 2, 0.1196, "304SS"),
("PT23", 1, 0.0598, "304SS"),
("PT24", 1, 0.0598, "304SS"),
("PT26", 4, 0.250, "304SS"),
("PT27", 2, 0.250, "304SS"),
("PT28", 4, 0.250, "304SS"),
("PT29", 6, 0.250, "304SS"),
("PT33", 2, 0.250, "304SS"),
("PT34", 4, 0.250, "304SS"),
("PT35", 3, 0.1875, "304SS"),
("PT36", 4, 0.1875, "304SS"),
("PT37", 4, 0.1875, "304SS"),
("PT38", 4, 0.1875, "304SS"),
("PT39", 2, 0.0598, "304SS"),
("PT40", 4, 0.0598, "304SS"),
("PT41", 1, 0.1875, "304SS"),
("PT43", 1, 0.0598, "304SS"),
("PT44", 1, 0.0598, "304SS"),
("PT45", 1, 0.250, "304SS"),
("PT46", 2, 0.250, "304SS"),
("PT47", 4, 0.250, "304SS"),
("PT48", 1, 0.250, "304SS"),
("PT49", 1, 0.750, "PCS"),
("PT50", 2, 0.375, "PCS"),
("PT51", 1, 0.250, "304SS"),
("PT52", 1, 0.1875, "304SS"),
("PT53", 1, 0.1875, "304SS"),
("PT54", 2, 0.250, "304SS"),
("PT55", 1, 0.1196, "304SS"),
("PT56", 1, 0.1196, "304SS"),
("PT57", 1, 0.0598, "304SS"),
("PT58", 1, 0.0598, "304SS"),
};
// Group by material + thickness
var groups = bom.GroupBy(b => (b.Material, b.Thickness)).OrderBy(g => g.Key.Material).ThenBy(g => g.Key.Thickness);
foreach (var group in groups)
{
var material = group.Key.Material;
var thickness = group.Key.Thickness;
var thicknessLabel = thickness switch
{
0.0598 => "16GA",
0.1196 => "11GA",
0.1875 => "3-16",
0.250 => "1-4",
0.375 => "3-8",
0.625 => "5-8",
0.750 => "3-4",
_ => thickness.ToString("F4")
};
var nestName = $"4526 A14 - {material} {thicknessLabel}";
Console.WriteLine($"\n=== {nestName} ===");
var nest = new Nest();
nest.Name = nestName;
nest.PlateDefaults.Thickness = thickness;
nest.PlateDefaults.Material = new Material { Name = material };
nest.PlateDefaults.PartSpacing = 0.125;
nest.PlateDefaults.EdgeSpacing = new Spacing(0.25, 0.25, 0.25, 0.25);
// Import DXFs for this group
var importer = new DxfImporter();
var colorIndex = 0;
double maxMinDim = 0;
double maxMaxDim = 0;
foreach (var item in group)
{
var dxfPath = Path.Combine(templateDir, $"4526 A14 {item.File}.dxf");
if (!File.Exists(dxfPath))
{
Console.WriteLine($" WARNING: {dxfPath} not found, skipping");
continue;
}
if (!importer.GetGeometry(dxfPath, out var geometry) || geometry.Count == 0)
{
Console.WriteLine($" WARNING: no geometry in {item.File}, skipping");
continue;
}
var pgm = ConvertGeometry.ToProgram(geometry);
if (pgm == null)
{
Console.WriteLine($" WARNING: failed to convert {item.File}, skipping");
continue;
}
var drawing = new Drawing(item.File, pgm);
drawing.Quantity.Required = item.Qty;
drawing.Material = new Material { Name = material };
drawing.Color = partColors[colorIndex % partColors.Length];
colorIndex++;
nest.Drawings.Add(drawing);
var bbox = pgm.BoundingBox();
var minDim = System.Math.Min(bbox.Width, bbox.Length);
var maxDim = System.Math.Max(bbox.Width, bbox.Length);
maxMinDim = System.Math.Max(maxMinDim, minDim);
maxMaxDim = System.Math.Max(maxMaxDim, maxDim);
Console.WriteLine($" {item.File}: {bbox.Width:F2} x {bbox.Length:F2}, qty={item.Qty}");
}
// Choose plate size based on largest part dimensions
// Size(width, length) — width is the short side, length is the long side
// Standard sizes: 48x96, 48x120, 60x120, 60x144, 72x144, 96x120
double plateW, plateL;
if (maxMinDim <= 47.5 && maxMaxDim <= 95.5)
{
plateW = 48; plateL = 96;
}
else if (maxMinDim <= 47.5 && maxMaxDim <= 119.5)
{
plateW = 48; plateL = 120;
}
else if (maxMinDim <= 59.5 && maxMaxDim <= 119.5)
{
plateW = 60; plateL = 120;
}
else if (maxMinDim <= 59.5 && maxMaxDim <= 143.5)
{
plateW = 60; plateL = 144;
}
else if (maxMinDim <= 71.5 && maxMaxDim <= 143.5)
{
plateW = 72; plateL = 144;
}
else if (maxMinDim <= 95.5 && maxMaxDim <= 119.5)
{
plateW = 96; plateL = 120;
}
else
{
// Fallback: round up to nearest 12"
plateW = System.Math.Ceiling((maxMinDim + 1) / 12.0) * 12;
plateL = System.Math.Ceiling((maxMaxDim + 1) / 12.0) * 12;
}
// Create one empty plate via PlateDefaults so it inherits settings
nest.PlateDefaults.Size = new Size(plateW, plateL);
var plate = nest.CreatePlate();
plate.Quantity = 1;
Console.WriteLine($" Plate size: {plateW} x {plateL} (W x L)");
Console.WriteLine($" Drawings: {nest.Drawings.Count}");
var outputPath = Path.Combine(outputDir, $"{nestName}.nest");
var writer = new NestWriter(nest);
if (writer.Write(outputPath))
Console.WriteLine($" Saved: {outputPath}");
else
Console.WriteLine($" ERROR: failed to save {outputPath}");
}
Console.WriteLine("\nDone!");