Compare commits
5 Commits
0246073b31
...
b970629a59
| Author | SHA1 | Date | |
|---|---|---|---|
| b970629a59 | |||
| 072915abf2 | |||
| aeeb2e4074 | |||
| a2f7219db3 | |||
| 7e4040ba08 |
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -458,6 +458,7 @@ namespace OpenNest.Forms
|
||||
PlateView.ZoomToPlate();
|
||||
PlateView.Refresh();
|
||||
UpdatePlateList();
|
||||
UpdatePlateHeader();
|
||||
}
|
||||
|
||||
public void SelectAllParts()
|
||||
|
||||
Generated
+26
-3
@@ -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
@@ -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>
|
||||
|
||||
@@ -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 |
@@ -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>
|
||||
@@ -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!");
|
||||
Reference in New Issue
Block a user