Files
OpenNest/OpenNest.Posts.Cincinnati/CincinnatiPostProcessor.cs
AJ Isaacs 0e299d7f6f feat(cincinnati): seed material library defaults and add selector dropdown
Adds the full Cincinnati material/etch library list as the committed
default config (seeded into Posts/ on build only when no runtime config
exists), plus a Selected Library override in the PropertyGrid backed by
a TypeConverter that populates from MaterialLibraries. MainForm calls
the new IPostProcessorNestAware hook before showing the config so the
dropdown opens preselected to the best match by nest material and
nearest thickness.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 06:16:29 -04:00

308 lines
12 KiB
C#

using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using OpenNest.CNC;
namespace OpenNest.Posts.Cincinnati
{
public sealed class CincinnatiPostProcessor : IConfigurablePostProcessor, IPostProcessorNestAware, IMaterialProvidingPostProcessor
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
WriteIndented = true,
Converters = { new JsonStringEnumConverter() }
};
public string Name => "Cincinnati CL-707";
public string Author => "OpenNest";
public string Description => "Cincinnati CL-707/CL-800/CL-900/CL-940/CLX family";
public CincinnatiPostConfig Config { get; }
object IConfigurablePostProcessor.Config => Config;
public IEnumerable<string> GetMaterialNames()
{
if (Config?.MaterialLibraries == null)
return System.Array.Empty<string>();
return Config.MaterialLibraries
.Select(e => e.Material)
.Where(s => !string.IsNullOrWhiteSpace(s));
}
public void PrepareForNest(Nest nest)
{
var materialName = nest?.Material?.Name ?? "";
var thickness = nest?.Thickness ?? 0.0;
Config.SelectedLibrary = Config.FindBestLibrary(materialName, thickness);
}
public CincinnatiPostProcessor()
{
var configPath = GetConfigPath();
if (File.Exists(configPath))
{
var json = File.ReadAllText(configPath);
Config = JsonSerializer.Deserialize<CincinnatiPostConfig>(json, JsonOptions);
}
else
{
Config = new CincinnatiPostConfig();
SaveConfig();
}
}
public CincinnatiPostProcessor(CincinnatiPostConfig config)
{
Config = config;
}
public void SaveConfig()
{
var configPath = GetConfigPath();
var json = JsonSerializer.Serialize(Config, JsonOptions);
File.WriteAllText(configPath, json);
}
private static string GetConfigPath()
{
var assemblyPath = typeof(CincinnatiPostProcessor).Assembly.Location;
var dir = Path.GetDirectoryName(assemblyPath);
var name = Path.GetFileNameWithoutExtension(assemblyPath);
return Path.Combine(dir, name + ".json");
}
public void Post(Nest nest, Stream outputStream)
{
// 1. Create variable manager and register standard variables
var vars = CreateVariableManager();
// 2. Filter to non-empty plates
var plates = nest.Plates
.Where(p => p.Parts.Count > 0)
.ToList();
// 3. Register user variables from drawing programs
var userVarMapping = RegisterUserVariables(vars, plates);
// 4. 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 nest material/thickness for preamble
var firstPlate = plates.FirstOrDefault();
var initialCutLibrary = resolver.ResolveCutLibrary(nest.Material?.Name ?? "", nest.Thickness, gas);
// 5. Build part sub-program registry (if enabled)
Dictionary<(int, long), int> partSubprograms = null;
List<(int subNum, string name, Program program)> subprogramEntries = null;
if (Config.UsePartSubprograms)
(partSubprograms, subprogramEntries) = CincinnatiPartSubprogramWriter.BuildRegistry(plates, Config.PartSubprogramStart);
// 5b. Build hole sub-program registry (SubProgramCalls across all parts)
var holeStartNumber = Config.PartSubprogramStart
+ (subprogramEntries?.Count ?? 0);
var (holeMapping, holeEntries) = CincinnatiPartSubprogramWriter.BuildHoleRegistry(plates, holeStartNumber);
// 6. Create writers
var preamble = new CincinnatiPreambleWriter(Config);
var sheetWriter = new CincinnatiSheetWriter(Config, vars,
holeMapping.Count > 0 ? holeMapping : null);
// 7. Build material description from nest
var material = nest.Material;
var materialDesc = material != null
? $"{material.Name}{(string.IsNullOrEmpty(material.Grade) ? "" : $", {material.Grade}")}"
: "";
// 8. Write to stream
using var writer = new StreamWriter(outputStream, Encoding.UTF8, 1024, leaveOpen: true);
// Main program
preamble.WriteMainProgram(writer, nest.Name ?? "NEST", materialDesc, plates, initialCutLibrary);
// Variable declaration subprogram
preamble.WriteVariableDeclaration(writer, vars);
// Sheet subprograms (one per unique layout, quantity handled via L count in main)
for (var i = 0; i < plates.Count; i++)
{
var plate = plates[i];
var layoutIndex = i + 1;
var subNumber = Config.SheetSubprogramStart + i;
var cutLibrary = resolver.ResolveCutLibrary(nest.Material?.Name ?? "", nest.Thickness, gas);
sheetWriter.Write(writer, plate, nest.Name ?? "NEST", layoutIndex, subNumber,
cutLibrary, etchLibrary, partSubprograms, userVarMapping);
}
// Part sub-programs (if enabled)
if (subprogramEntries != null)
{
var partSubWriter = new CincinnatiPartSubprogramWriter(Config,
holeMapping.Count > 0 ? holeMapping : null);
var sheetDiagonal = firstPlate != null
? System.Math.Sqrt(firstPlate.Size.Width * firstPlate.Size.Width
+ firstPlate.Size.Length * firstPlate.Size.Length)
: 100.0;
foreach (var (subNum, name, pgm) in subprogramEntries)
{
partSubWriter.Write(writer, pgm, name, subNum,
initialCutLibrary, etchLibrary, sheetDiagonal);
}
}
// Hole sub-programs (SubProgramCall definitions)
if (holeEntries.Count > 0)
{
var holeSubWriter = new CincinnatiPartSubprogramWriter(Config);
var sheetDiagonal = firstPlate != null
? System.Math.Sqrt(firstPlate.Size.Width * firstPlate.Size.Width
+ firstPlate.Size.Length * firstPlate.Size.Length)
: 100.0;
foreach (var (subNum, pgm) in holeEntries)
{
CincinnatiPartSubprogramWriter.EnsureLeadingRapid(pgm);
holeSubWriter.Write(writer, pgm, "HOLE", subNum,
initialCutLibrary, etchLibrary, sheetDiagonal);
}
}
writer.Flush();
}
public void Post(Nest nest, string outputFile)
{
using var fs = new FileStream(outputFile, FileMode.Create, FileAccess.Write);
Post(nest, fs);
}
private Dictionary<(int drawingId, string varName), int> RegisterUserVariables(
ProgramVariableManager vars, List<Plate> plates)
{
var mapping = new Dictionary<(int drawingId, string varName), int>();
var nextNumber = Config.UserVariableStart;
// Track global variables by name so they share a single number
var globalNumbers = new Dictionary<string, int>(System.StringComparer.OrdinalIgnoreCase);
// Collect unique drawings from all plates
var seenDrawings = new HashSet<int>();
foreach (var plate in plates)
{
foreach (var part in plate.Parts)
{
var drawing = part.BaseDrawing;
if (drawing.IsCutOff || !seenDrawings.Add(drawing.Id))
continue;
foreach (var kvp in drawing.Program.Variables)
{
var varDef = kvp.Value;
// Skip inline variables — they emit literal values
if (varDef.Inline)
continue;
if (varDef.Global)
{
if (!globalNumbers.TryGetValue(varDef.Name, out var globalNum))
{
globalNum = nextNumber++;
globalNumbers[varDef.Name] = globalNum;
// Register once in the variable manager
var commentName = ToPascalCase(varDef.Name);
var expression = FormatVariableValue(varDef.Value);
vars.GetOrCreate(commentName, globalNum, expression);
}
mapping[(drawing.Id, varDef.Name)] = globalNum;
}
else
{
var num = nextNumber++;
mapping[(drawing.Id, varDef.Name)] = num;
// Register with drawing name prefix in the comment
var drawingLabel = ToPascalCase(drawing.Name);
var varLabel = ToPascalCase(varDef.Name);
var commentName = $"{drawingLabel}{varLabel}";
var expression = FormatVariableValue(varDef.Value);
vars.GetOrCreate(commentName, num, expression);
}
}
}
}
return mapping;
}
/// <summary>
/// Converts a variable name from snake_case or camelCase to PascalCase.
/// Examples: "sheet_width" → "SheetWidth", "holeSpacing" → "HoleSpacing"
/// </summary>
private static string ToPascalCase(string name)
{
var sb = new StringBuilder(name.Length);
var capitalizeNext = true;
foreach (var c in name)
{
if (c == '_')
{
capitalizeNext = true;
continue;
}
if (capitalizeNext)
{
sb.Append(char.ToUpper(c));
capitalizeNext = false;
}
else
{
sb.Append(c);
}
}
return sb.ToString();
}
private static string FormatVariableValue(double value)
{
return value.ToString("0.####", System.Globalization.CultureInfo.InvariantCulture);
}
private ProgramVariableManager CreateVariableManager()
{
var vars = new ProgramVariableManager();
vars.GetOrCreate("ProcessFeedrate", 148); // Set by G89, no expression
vars.GetOrCreate("LeadInFeedrate", 126, $"[#148*{Config.LeadInFeedratePercent}]");
vars.GetOrCreate("LeadInArcLine2Feedrate", 127, $"[#148*{Config.LeadInArcLine2FeedratePercent}]");
vars.GetOrCreate("CircleFeedrate", 128, Config.CircleFeedrateMultiplier.ToString("0.#"));
vars.GetOrCreate("LeadOutFeedrate", 129, $"[#148*{Config.LeadOutFeedratePercent}]");
if (Config.ArcFeedrate == ArcFeedrateMode.Variables)
{
foreach (var range in Config.ArcFeedrateRanges)
{
var name = $"ArcFeedR{range.MaxRadius.ToString("0.###", System.Globalization.CultureInfo.InvariantCulture)}";
vars.GetOrCreate(name, range.VariableNumber,
$"[#148*{range.FeedratePercent.ToString("0.##", System.Globalization.CultureInfo.InvariantCulture)}]");
}
}
return vars;
}
}
}