Files
OpenNest/OpenNest.Data/MachineCuttingDefaults.cs
T
ajandClaude Opus 5.5 3cd7729d38 feat(cutting): keep cutting settings per machine material and thickness
Tabs suit thin plate, but on anything thicker than 0.25 in the parts
are too hard to break out of the sheet, and the Cutting Parameters
dialog was one global set for every job. Its settings now live on the
nest's machine thickness: leads on the existing Outside/Inside Cuts
tabs, and lead-in angle rounding, tabs and pierce on a new Tabs &
Pierce tab (ThicknessConfig.Cutting). In Database mode every
workstation gets the same settings through the shared machine.

The dialog and the Place Lead-in panel say what they edit, and save
back to that thickness (MachineCuttingDefaults.WriteTo, converted to
the machine's units). A thickness with no Tabs & Pierce of its own, or
a nest the machine has no thickness for, keeps this PC's settings as
before. If the machine can't be saved (server offline, a conflict),
the edit is kept on this PC for that job and the user is told.
Machine Configuration can copy one thickness's Tabs & Pierce to
thinner or thicker ones, so the 0.25 in rule takes a few clicks.

An older desktop saving a shared machine would silently erase these
settings, since it doesn't know them. Machine configuration classes
now keep unknown fields, and the server refuses a machine save that
leaves out a field the stored machine has a value for, telling the
user to update OpenNest.

The Plan Cutting status line now reports tabs from SupportTabs; since
"Tab every part" was retired it always said off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 08:24:39 -05:00

419 lines
20 KiB
C#

using OpenNest.CNC.CuttingStrategy;
using OpenNest.Geometry;
namespace OpenNest.Data;
/// <summary>
/// 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.
/// </summary>
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;
/// <summary>
/// The thickness settings of <paramref name="machine"/> 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.
/// </summary>
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();
}
/// <summary>
/// Replaces the lead-ins, lead-outs and kerf of <paramref name="parameters"/> with the
/// machine's for the nest, and its lead-in angle rounding, tabs and pierce when the thickness
/// sets them (<see cref="ThicknessConfig.Cutting"/>), and records the machine, material and
/// thickness they came from. Returns false, changing nothing, when the machine has no
/// settings for the nest.
/// </summary>
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;
}
/// <summary>
/// Stores the leads, lead-in angle rounding, tabs and pierce of <paramref name="parameters"/>
/// (in the nest's units) on the machine's thickness for the nest, in the machine's units: the
/// inverse of <see cref="ApplyTo(CuttingParameters, MachineConfig?, Nest)"/>. 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.
/// </summary>
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;
}
/// <summary>
/// The thickness's lead-in angle rounding, tabs and pierce as cutting parameters in the
/// machine's units, for editing them; <see cref="ThicknessCuttingConfig"/>'s defaults when it
/// sets none.
/// </summary>
public static CuttingParameters CuttingOf(ThicknessCuttingConfig? cutting)
{
var parameters = new CuttingParameters();
ApplyCutting(parameters, cutting ?? new ThicknessCuttingConfig(), 1);
return parameters;
}
/// <summary>The thickness settings form of <paramref name="parameters"/>' rounding, tabs and pierce, unscaled.</summary>
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,
};
/// <summary>
/// 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.
/// </summary>
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;
}
/// <summary>
/// Runs <paramref name="edit"/>, then, when it changed the nest's material, grade or
/// thickness, applies the machine's spacing for them (<see cref="ApplySpacingTo"/>). A
/// spacing the edit changed itself is the user's choice and stays.
/// </summary>
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;
}
/// <summary>
/// The leads <paramref name="settings"/> 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.
/// </summary>
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);
}
/// <summary>
/// Whether <paramref name="parameters"/> are stamped with the machine, material, grade and
/// thickness <see cref="ApplyTo(CuttingParameters, MachineConfig?, Nest)"/> 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.
/// </summary>
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;
}
/// <summary>
/// Replaces the part clearance, overtravel, minimum segment and direction of
/// <paramref name="settings"/> 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.
/// </summary>
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<CutDirection>(cutOff.Direction, true, out var direction) && Enum.IsDefined(direction))
settings.CutDirection = direction;
return true;
}
/// <summary>
/// The lead-in <paramref name="lead"/> describes, in the machine's units, with
/// <paramref name="kerf"/> for a clean hole; null for a type it cannot build (Line + Line).
/// </summary>
public static LeadIn? LeadInOf(LeadConfig lead, double kerf) => ToLeadIn(lead, 1, kerf);
/// <summary>The lead-out <paramref name="lead"/> describes, in the machine's units.</summary>
public static LeadOut LeadOutOf(LeadConfig lead) => ToLeadOut(lead, 1);
/// <summary>
/// The machine configuration's form of <paramref name="leadIn"/>; what it cannot hold (an
/// arc's sweep, a clean hole's kerf, a second line) is left out. Unset fields keep
/// <paramref name="previous"/>'s values.
/// </summary>
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;
}
/// <summary>The machine configuration's form of <paramref name="leadOut"/>.</summary>
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,
};
}