using System; using System.Collections.Generic; using System.ComponentModel; using OpenNest.PostSettings; namespace OpenNest.Posts.CincinnatiCIFiber { /// /// Configuration for the Cincinnati CI Fiber (TF5200 / nLight CLX) post-processor. /// All layer macro names, header variable names and the material code map are /// editable so the post can serve other CI Fiber tables, not only the 4020. /// [PostSettingsSection( MachineSection, 0, Description = "Which CI Fiber table this configuration drives and the largest sheet it accepts." )] [PostSettingsSection( MaterialSection, 1, Description = "Maps OpenNest material names to the controller's V.E.MATERIAL code." )] [PostSettingsSection( OutputSection, 2, Description = "Number format, units, and what the posted program includes." )] [PostSettingsSection( MacrosSection, 3, Description = "Controller subroutines called around each contour and at program start and end. They must exist on the machine." )] public class CIFiberPostConfig { private const string MachineSection = "Machine"; private const string MaterialSection = "Material"; private const string OutputSection = "Program output"; private const string MacrosSection = "Macros"; /// Emitted in the "( CONFIGURATION - ... )" header comment. [DisplayName("Configuration name")] [Description("Shown in the program header comment ( CONFIGURATION - ... ).")] [PostSetting(MachineSection, 0)] public string ConfigurationName { get; set; } = "CI FIBER 8K"; /// /// Decimal places for posted coordinates and header values. The sample /// machine program posts 3 decimals for inch units. /// [DisplayName("Posted accuracy")] [Description("Decimal places for coordinates and header values. The machine sample uses 3 for inches.")] [PostSetting(OutputSection, 0, Minimum = 0, Maximum = 6)] public int PostedAccuracy { get; set; } = 3; /// Value for V.E.UNIT when the nest is in inches. [DisplayName("Unit code (inches)")] [Description("V.E.UNIT value written for inch nests.")] [PostSetting(OutputSection, 1, Minimum = 0, Maximum = 99)] public int InchUnitCode { get; set; } = 1; /// /// Value for V.E.UNIT when the nest is in millimeters. The TF5200 /// metric unit code is unconfirmed; adjust if the controller rejects 0. /// [DisplayName("Unit code (millimeters)")] [Description("V.E.UNIT value written for millimeter nests. Unconfirmed on the controller.")] [PostSetting(OutputSection, 2, Minimum = 0, Maximum = 99)] public int MetricUnitCode { get; set; } = 0; /// Emit the V.E.SHEET_WEIGHT header variable (value 0). [DisplayName("Emit sheet weight")] [Description("Write the V.E.SHEET_WEIGHT header variable (value 0).")] [PostSetting(OutputSection, 3)] public bool EmitSheetWeight { get; set; } = false; /// Skip contours whose moves are all LayerType.Scribe (marks). [DisplayName("Skip scribe layer")] [Description("Leave out contours that are entirely scribe/etch marks.")] [PostSetting(OutputSection, 4)] public bool SkipScribe { get; set; } = true; /// Maximum plate length (X) this configuration drives; 0 disables the check. [DisplayName("Maximum table X")] [Description("Largest plate length (X) this table accepts. Posting a larger plate fails. 0 disables the check.")] [PostSetting(MachineSection, 1, Minimum = 0, Maximum = 10000, DecimalPlaces = 3)] public double MaxTableX { get; set; } = 160.25; /// Maximum plate width (Y) this configuration drives; 0 disables the check. [DisplayName("Maximum table Y")] [Description("Largest plate width (Y) this table accepts. Posting a larger plate fails. 0 disables the check.")] [PostSetting(MachineSection, 2, Minimum = 0, Maximum = 10000, DecimalPlaces = 3)] public double MaxTableY { get; set; } = 81.25; /// Skippable global subroutine used to cancel comp / park between features. [DisplayName("Layer: cancel (L0)")] [Description("Called before each contour and at the end to cancel compensation and park.")] [PostSetting(MacrosSection, 0)] public string LayerCancel { get; set; } = "L0"; /// Skippable global subroutine for interior lead-in plus G41. [DisplayName("Layer: interior leadin (L2)")] [Description("Called before an interior (hole) lead-in, with G41.")] [PostSetting(MacrosSection, 1)] public string LayerInteriorLeadin { get; set; } = "L2"; /// Skippable global subroutine for exterior lead-in plus G42. [DisplayName("Layer: exterior leadin (L4)")] [Description("Called before an exterior (perimeter) lead-in, with G42.")] [PostSetting(MacrosSection, 2)] public string LayerExteriorLeadin { get; set; } = "L4"; /// Skippable global subroutine that switches the cut layer on. [DisplayName("Layer: cut on (L6)")] [Description("Switches the cut layer on after the lead-in.")] [PostSetting(MacrosSection, 3)] public string LayerCut { get; set; } = "L6"; /// Skippable global subroutine at the end of each contour (head separation / comp off). [DisplayName("Layer: cut end (ZHSOFF)")] [Description("Called at the end of each contour (head separation).")] [PostSetting(MacrosSection, 4)] public string LayerCutEnd { get; set; } = "ZHSOFF"; /// External program called once at program start. [DisplayName("Start macro")] [Description("Program called once at the start (L name).")] [PostSetting(MacrosSection, 5)] public string ProgramStartMacro { get; set; } = "PROGRAMSTART.NC"; /// External program called after the last sheet. [DisplayName("End macro")] [Description("Program called after the last sheet (L name).")] [PostSetting(MacrosSection, 6)] public string ProgramEndMacro { get; set; } = "PROGRAMEND.NC"; /// /// Material name (case-insensitive) to machine material code map for /// V.E.MATERIAL. The setter re-keys any assigned map (including one /// deserialized from JSON, which is case-sensitive) case-insensitively; /// if names collide by case, the later entry wins. /// [DisplayName("Material codes")] [Description("Material name (not case-sensitive) and the code written to V.E.MATERIAL.")] [PostSetting(MaterialSection, 0, KeyHeader = "Material name", ValueHeader = "Machine code")] public Dictionary MaterialCodes { get => _materialCodes; set => _materialCodes = IgnoreCase(value); } private Dictionary _materialCodes = new(StringComparer.OrdinalIgnoreCase) { ["Mild Steel"] = "MSN", }; private static Dictionary IgnoreCase(Dictionary map) { if (map == null || map.Comparer == StringComparer.OrdinalIgnoreCase) return map; var copy = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var entry in map) copy[entry.Key] = entry.Value; return copy; } /// Fallback V.E.MATERIAL code when the material name has no mapping. [DisplayName("Default material code")] [Description("Code used when the nest material has no entry in the table above.")] [PostSetting(MaterialSection, 1)] public string DefaultMaterialCode { get; set; } = "MSN"; public string ResolveMaterialCode(string materialName) { if (!string.IsNullOrWhiteSpace(materialName) && MaterialCodes != null && MaterialCodes.TryGetValue(materialName.Trim(), out var code) && !string.IsNullOrWhiteSpace(code)) return code; return DefaultMaterialCode ?? "MSN"; } /// /// Throws if the plate exceeds the configured table envelope. /// Zero limits disable the check. /// public void ValidateTableSize(double lengthX, double widthY) { if (MaxTableX > 0 && lengthX > MaxTableX + ToleranceEpsilon) throw new InvalidOperationException( $"Plate length {lengthX:0.###} exceeds maximum table X {MaxTableX:0.###}." ); if (MaxTableY > 0 && widthY > MaxTableY + ToleranceEpsilon) throw new InvalidOperationException( $"Plate width {widthY:0.###} exceeds maximum table Y {MaxTableY:0.###}." ); } private const double ToleranceEpsilon = 1e-6; } }