using OpenNest.CNC.CuttingStrategy; using OpenNest.Geometry; namespace OpenNest.Data; /// /// The lead-ins, lead-outs, kerf and cut-off a machine configuration has on file for a nest's /// material and thickness, converted to the nest's units. Outside cuts take the thickness's /// lead-in and lead-out; inside cuts (circles included) take its inside ones, or the outside /// ones when it has none. A Clean Hole lead-in is for holes only, so on outside cuts it leaves /// the external lead-in as it was, and a Line + Line lead-in, whose second segment the /// configuration does not hold, leaves the lead-in it would set as it was. /// public static class MachineCuttingDefaults { private const double MillimetersPerInch = 25.4; // Thicknesses are typed or imported to three decimals (PEP stores 3/16 as 0.188), so this // matches those and ones converted between units; gauges are far further apart. private const double ThicknessToleranceInches = 0.001; /// /// The thickness settings of for the nest's material (its grade /// when one is set, otherwise the first grade on file) and thickness, or null when the /// machine has none. /// public static ThicknessConfig? Find(MachineConfig? machine, Nest nest) { ArgumentNullException.ThrowIfNull(nest); var name = nest.Material?.Name; if (machine == null || string.IsNullOrWhiteSpace(name) || !double.IsFinite(nest.Thickness) || nest.Thickness <= 0) return null; var grade = nest.Material.Grade; var material = string.IsNullOrWhiteSpace(grade) ? machine.GetMaterial(name) : machine.GetMaterial(name, grade); if (material == null) return null; var thickness = nest.Thickness * Scale(nest.Units, machine.Units); var tolerance = ThicknessToleranceInches * Scale(Units.Inches, machine.Units); return material.Thicknesses .Where(t => System.Math.Abs(t.Value - thickness) <= tolerance) .OrderBy(t => System.Math.Abs(t.Value - thickness)) .FirstOrDefault(); } /// /// Replaces the lead-ins, lead-outs and kerf of with the /// machine's for the nest, and its lead-in angle rounding, tabs and pierce when the thickness /// sets them (), and records the machine, material and /// thickness they came from. Returns false, changing nothing, when the machine has no /// settings for the nest. /// public static bool ApplyTo(CuttingParameters parameters, MachineConfig? machine, Nest nest) { ArgumentNullException.ThrowIfNull(parameters); if (Find(machine, nest) is not { } settings) return false; var scale = Scale(machine!.Units, nest.Units); ApplyLeads(parameters, settings, scale); parameters.Kerf = settings.Kerf * scale; if (settings.Cutting is { } cutting) ApplyCutting(parameters, cutting, scale); parameters.MachineName = machine.Name; parameters.MaterialName = nest.Material.Name; parameters.Grade = nest.Material.Grade; parameters.Thickness = nest.Thickness; return true; } /// /// Stores the leads, lead-in angle rounding, tabs and pierce of /// (in the nest's units) on the machine's thickness for the nest, in the machine's units: the /// inverse of . Kerf, cut-off, /// spacing and plate sizes are kept. Inside cuts that match the outside ones keep following /// them. Returns false, changing nothing, when the machine has no settings for the nest. /// public static bool WriteTo(MachineConfig? machine, Nest nest, CuttingParameters parameters) { ArgumentNullException.ThrowIfNull(parameters); if (Find(machine, nest) is not { } settings) return false; var scale = Scale(nest.Units, machine!.Units); // A lead the configuration cannot hold (Line + Line, which it never applies) leaves the // lead as it was; so does a Clean Hole on outside cuts, which is for holes only. var insideFrom = settings.InsideLeadIn ?? settings.LeadIn; var outsideIn = parameters.ExternalLeadIn is LineLineLeadIn or CleanHoleLeadIn ? settings.LeadIn : ToConfig(Scaled(parameters.ExternalLeadIn, scale), settings.LeadIn); var outsideOut = ToConfig(Scaled(parameters.ExternalLeadOut, scale), settings.LeadOut); var insideIn = parameters.InternalLeadIn is LineLineLeadIn ? insideFrom : ToConfig(Scaled(parameters.InternalLeadIn, scale), insideFrom); var insideOut = ToConfig(Scaled(parameters.InternalLeadOut, scale), settings.InsideLeadOut ?? settings.LeadOut); settings.LeadIn = outsideIn; settings.LeadOut = outsideOut; var follows = SameLead(insideIn, settings.LeadIn) && SameLead(insideOut, settings.LeadOut); settings.InsideLeadIn = follows ? null : insideIn; settings.InsideLeadOut = follows ? null : insideOut; settings.Cutting = ToCuttingConfig(parameters, scale, settings.Cutting); return true; } /// /// The thickness's lead-in angle rounding, tabs and pierce as cutting parameters in the /// machine's units, for editing them; 's defaults when it /// sets none. /// public static CuttingParameters CuttingOf(ThicknessCuttingConfig? cutting) { var parameters = new CuttingParameters(); ApplyCutting(parameters, cutting ?? new ThicknessCuttingConfig(), 1); return parameters; } /// The thickness settings form of ' rounding, tabs and pierce, unscaled. public static ThicknessCuttingConfig ToCuttingConfig(CuttingParameters parameters, ThicknessCuttingConfig? previous = null) => ToCuttingConfig(parameters, 1, previous); private static ThicknessCuttingConfig ToCuttingConfig(CuttingParameters parameters, double scale, ThicknessCuttingConfig? previous) => new() { RoundLeadInAngles = parameters.RoundLeadInAngles, LeadInAngleIncrement = parameters.LeadInAngleIncrement, Tabs = parameters.SupportTabs, TabBySize = parameters.TabBySize, TabSize = (parameters.TabConfig?.Size ?? previous?.TabSize ?? new ThicknessCuttingConfig().TabSize) * scale, TabHeldMaxLength = parameters.TabHeldMaxLength * scale, TabSizeMinWidth = parameters.TabSizeMinWidth * scale, TabSizeMaxWidth = parameters.TabSizeMaxWidth * scale, PierceClearance = parameters.PierceClearance * scale, PierceDiameter = parameters.PierceDiameter * scale, Unknown = previous?.Unknown, }; private static void ApplyCutting(CuttingParameters parameters, ThicknessCuttingConfig cutting, double scale) { parameters.RoundLeadInAngles = cutting.RoundLeadInAngles; parameters.LeadInAngleIncrement = cutting.LeadInAngleIncrement; parameters.SupportTabs = cutting.Tabs; parameters.TabBySize = cutting.TabBySize; // The thickness's tabs replace any retired "Tab every part" the fallback still carries. parameters.TabsEnabled = false; parameters.TabConfig = new NormalTab { Size = cutting.TabSize * scale }; parameters.TabHeldMaxLength = cutting.TabHeldMaxLength * scale; parameters.TabSizeMinWidth = cutting.TabSizeMinWidth * scale; parameters.TabSizeMaxWidth = cutting.TabSizeMaxWidth * scale; parameters.PierceClearance = cutting.PierceClearance * scale; parameters.PierceDiameter = cutting.PierceDiameter * scale; } // Equal for the fields the lead's type uses; ToConfig carries the others over from before. private static bool SameLead(LeadConfig a, LeadConfig b) { if (a.Type != b.Type) return false; var (length, angle, radius) = a.Type switch { "Line" => (true, true, false), "Arc" => (false, false, true), "Line + Arc" => (true, true, true), "Clean Hole" => (true, false, true), "Line + Line" => (true, true, false), _ => (false, false, false), }; return (!length || a.Length.Equals(b.Length)) && (!angle || a.Angle.Equals(b.Angle)) && (!radius || a.Radius.Equals(b.Radius)); } // A lead with its lengths converted; angles and the arc sweep are not lengths. private static LeadIn Scaled(LeadIn leadIn, double scale) => scale == 1 ? leadIn : leadIn switch { LineLeadIn line => new LineLeadIn { Length = line.Length * scale, ApproachAngle = line.ApproachAngle }, ArcLeadIn arc => new ArcLeadIn { Radius = arc.Radius * scale, SweepAngle = arc.SweepAngle }, LineArcLeadIn lineArc => new LineArcLeadIn { LineLength = lineArc.LineLength * scale, ArcRadius = lineArc.ArcRadius * scale, ApproachAngle = lineArc.ApproachAngle, }, CleanHoleLeadIn cleanHole => new CleanHoleLeadIn { LineLength = cleanHole.LineLength * scale, ArcRadius = cleanHole.ArcRadius * scale, Kerf = cleanHole.Kerf * scale, }, LineLineLeadIn lineLine => new LineLineLeadIn { Length1 = lineLine.Length1 * scale, ApproachAngle1 = lineLine.ApproachAngle1, Length2 = lineLine.Length2 * scale, ApproachAngle2 = lineLine.ApproachAngle2, }, _ => leadIn, }; private static LeadOut Scaled(LeadOut leadOut, double scale) => scale == 1 ? leadOut : leadOut switch { LineLeadOut line => new LineLeadOut { Length = line.Length * scale, ApproachAngle = line.ApproachAngle }, ArcLeadOut arc => new ArcLeadOut { Radius = arc.Radius * scale }, _ => leadOut, }; /// /// Gives the nest's plate defaults the part and edge spacing the machine has for its /// material and thickness. Spacing the machine leaves unset stays as it was. Returns false, /// changing nothing, when the machine sets neither. /// public static bool ApplySpacingTo(Nest nest, MachineConfig? machine) { ArgumentNullException.ThrowIfNull(nest); var settings = Find(machine, nest); if (settings?.PartSpacing == null && settings?.EdgeSpacing == null) return false; var scale = Scale(machine!.Units, nest.Units); if (settings.PartSpacing is { } partSpacing) nest.PlateDefaults.PartSpacing = partSpacing * scale; if (settings.EdgeSpacing is { } edge) nest.PlateDefaults.EdgeSpacing = new Spacing(edge.Left * scale, edge.Bottom * scale, edge.Right * scale, edge.Top * scale); return true; } /// /// Runs , then, when it changed the nest's material, grade or /// thickness, applies the machine's spacing for them (). A /// spacing the edit changed itself is the user's choice and stays. /// public static void ApplySpacingAfter(Nest nest, MachineConfig? machine, Action edit) { ArgumentNullException.ThrowIfNull(nest); ArgumentNullException.ThrowIfNull(edit); var job = (nest.Material?.Name, nest.Material?.Grade, nest.Thickness); var plate = nest.PlateDefaults; var (partBefore, edgeBefore) = (plate.PartSpacing, plate.EdgeSpacing); edit(); if (job == (nest.Material?.Name, nest.Material?.Grade, nest.Thickness)) return; plate = nest.PlateDefaults; var (partEdited, edgeEdited) = (plate.PartSpacing, plate.EdgeSpacing); ApplySpacingTo(nest, machine); if (partEdited != partBefore) plate.PartSpacing = partEdited; if (!edgeEdited.Equals(edgeBefore)) plate.EdgeSpacing = edgeEdited; } /// /// The leads give, in the machine's units, for previewing them: /// a lead the machine leaves as it was (Line + Line, or Clean Hole on outside cuts) is none. /// public static CuttingParameters LeadsOf(ThicknessConfig settings) { ArgumentNullException.ThrowIfNull(settings); var parameters = new CuttingParameters { ExternalLeadIn = new NoLeadIn(), InternalLeadIn = new NoLeadIn() }; ApplyLeads(parameters, settings, 1); parameters.Kerf = settings.Kerf; return parameters; } private static void ApplyLeads(CuttingParameters parameters, ThicknessConfig settings, double scale) { var kerf = settings.Kerf * scale; // Each lead set gets its own instance, as the cutting panel builds them. if (ToLeadIn(settings.LeadIn, scale, kerf) is { } leadIn and not CleanHoleLeadIn) parameters.ExternalLeadIn = leadIn; if (ToLeadIn(settings.InsideLeadIn ?? settings.LeadIn, scale, kerf) is { } insideLeadIn) parameters.InternalLeadIn = insideLeadIn; parameters.ExternalLeadOut = ToLeadOut(settings.LeadOut, scale); parameters.InternalLeadOut = ToLeadOut(settings.InsideLeadOut ?? settings.LeadOut, scale); } /// /// Whether are stamped with the machine, material, grade and /// thickness would record for /// the nest: they started from the machine's settings for it, and any lead edits since are /// the user's choice for that same job, which the machine's settings must not replace. /// public static bool IsRecordedFor(CuttingParameters parameters, MachineConfig? machine, Nest nest) { ArgumentNullException.ThrowIfNull(parameters); ArgumentNullException.ThrowIfNull(nest); if (Find(machine, nest) == null) return false; var tolerance = ThicknessToleranceInches * Scale(Units.Inches, nest.Units); return string.Equals(parameters.MachineName, machine!.Name, StringComparison.Ordinal) && string.Equals(parameters.MaterialName, nest.Material.Name, StringComparison.Ordinal) && string.Equals(parameters.Grade ?? "", nest.Material.Grade ?? "", StringComparison.Ordinal) && System.Math.Abs(parameters.Thickness - nest.Thickness) <= tolerance; } /// /// Replaces the part clearance, overtravel, minimum segment and direction of /// with the machine's for the nest; the head-lock choice is kept. /// Returns false, changing nothing, when the machine has no settings for the nest. /// public static bool ApplyTo(CutOffSettings settings, MachineConfig? machine, Nest nest) { ArgumentNullException.ThrowIfNull(settings); if (Find(machine, nest)?.CutOff is not { } cutOff) return false; var scale = Scale(machine!.Units, nest.Units); settings.PartClearance = cutOff.PartClearance * scale; settings.Overtravel = cutOff.Overtravel * scale; settings.MinSegmentLength = cutOff.MinSegmentLength * scale; if (Enum.TryParse(cutOff.Direction, true, out var direction) && Enum.IsDefined(direction)) settings.CutDirection = direction; return true; } /// /// The lead-in describes, in the machine's units, with /// for a clean hole; null for a type it cannot build (Line + Line). /// public static LeadIn? LeadInOf(LeadConfig lead, double kerf) => ToLeadIn(lead, 1, kerf); /// The lead-out describes, in the machine's units. public static LeadOut LeadOutOf(LeadConfig lead) => ToLeadOut(lead, 1); /// /// The machine configuration's form of ; what it cannot hold (an /// arc's sweep, a clean hole's kerf, a second line) is left out. Unset fields keep /// 's values. /// public static LeadConfig ToConfig(LeadIn leadIn, LeadConfig? previous = null) { var config = Copy(previous); switch (leadIn) { case LineLeadIn line: (config.Type, config.Length, config.Angle) = ("Line", line.Length, line.ApproachAngle); break; case ArcLeadIn arc: (config.Type, config.Radius) = ("Arc", arc.Radius); break; case LineArcLeadIn lineArc: (config.Type, config.Length, config.Radius, config.Angle) = ("Line + Arc", lineArc.LineLength, lineArc.ArcRadius, lineArc.ApproachAngle); break; case CleanHoleLeadIn cleanHole: (config.Type, config.Length, config.Radius) = ("Clean Hole", cleanHole.LineLength, cleanHole.ArcRadius); break; case LineLineLeadIn lineLine: (config.Type, config.Length, config.Angle) = ("Line + Line", lineLine.Length1, lineLine.ApproachAngle1); break; default: config.Type = "None"; break; } return config; } /// The machine configuration's form of . public static LeadConfig ToConfig(LeadOut leadOut, LeadConfig? previous = null) { var config = Copy(previous); switch (leadOut) { case LineLeadOut line: (config.Type, config.Length, config.Angle) = ("Line", line.Length, line.ApproachAngle); break; case ArcLeadOut arc: (config.Type, config.Radius) = ("Arc", arc.Radius); break; default: config.Type = "None"; break; } return config; } private static LeadConfig Copy(LeadConfig? lead) => lead == null ? new LeadConfig() : new LeadConfig { Type = lead.Type, Length = lead.Length, Angle = lead.Angle, Radius = lead.Radius }; // Lead types are the names Tools > Machine Configuration offers; null when one cannot be built. private static LeadIn? ToLeadIn(LeadConfig? lead, double scale, double kerf) => lead?.Type switch { null or "None" => new NoLeadIn(), "Line" => new LineLeadIn { Length = lead.Length * scale, ApproachAngle = lead.Angle }, "Arc" => new ArcLeadIn { Radius = lead.Radius * scale }, "Line + Arc" => new LineArcLeadIn { LineLength = lead.Length * scale, ArcRadius = lead.Radius * scale, ApproachAngle = lead.Angle, }, "Clean Hole" => new CleanHoleLeadIn { LineLength = lead.Length * scale, ArcRadius = lead.Radius * scale, Kerf = kerf }, _ => null, }; private static LeadOut ToLeadOut(LeadConfig? lead, double scale) => lead?.Type switch { "Line" => new LineLeadOut { Length = lead.Length * scale, ApproachAngle = lead.Angle }, "Arc" => new ArcLeadOut { Radius = lead.Radius * scale }, _ => new NoLeadOut(), }; private static double Scale(UnitSystem from, Units to) => Scale(from == UnitSystem.Millimeters ? Units.Millimeters : Units.Inches, to); private static double Scale(Units from, UnitSystem to) => Scale(from, to == UnitSystem.Millimeters ? Units.Millimeters : Units.Inches); private static double Scale(Units from, Units to) => (from, to) switch { (Units.Inches, Units.Millimeters) => MillimetersPerInch, (Units.Millimeters, Units.Inches) => 1 / MillimetersPerInch, _ => 1, }; }