feat(machines): split machine leads into outside and inside cuts

Machine Configuration held one lead-in and lead-out per thickness and
applied it to every contour. Each thickness now has Outside Cuts and
Inside Cuts tabs; inside cuts follow the outside leads until "Same as
outside cuts" is cleared, and configurations without inside leads keep
applying one pair to both.

The lead type lists, per-type fields and preview from Cutting Settings
are now a reusable LeadsEditor control that both dialogs use. The
machine tabs hide what the configuration cannot hold: the arc sweep,
the clean-hole kerf (taken from the thickness) and Line + Line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ajandClaude Opus 5.5 committed 2026-10-29 18:51:42 -04:00
1 parent 7ec11993de
commit bb21ffdf35
8 files changed
+957 -535

No files matched your search

+98 -17
View File
@@ -3,12 +3,12 @@ using OpenNest.CNC.CuttingStrategy;
namespace OpenNest.Data;
/// <summary>
/// The lead-in, lead-out, kerf and cut-off a machine configuration has on file for a nest's
/// material and thickness, converted to the nest's units. A machine has one lead-in and one
/// lead-out per thickness, so they apply to external, internal and arc/circle contours alike;
/// a Clean Hole lead-in is for holes only and leaves the external lead-in as it was, and a
/// Line + Line lead-in, whose second segment the configuration does not hold, leaves every
/// lead-in as it was.
/// 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
{
@@ -55,17 +55,8 @@ public static class MachineCuttingDefaults
return false;
var scale = Scale(machine!.Units, nest.Units);
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)
{
if (leadIn is not CleanHoleLeadIn)
parameters.ExternalLeadIn = leadIn;
parameters.InternalLeadIn = ToLeadIn(settings.LeadIn, scale, kerf);
}
parameters.ExternalLeadOut = ToLeadOut(settings.LeadOut, scale);
parameters.InternalLeadOut = ToLeadOut(settings.LeadOut, scale);
parameters.Kerf = kerf;
ApplyLeads(parameters, settings, scale);
parameters.Kerf = settings.Kerf * scale;
parameters.MachineName = machine.Name;
parameters.MaterialName = nest.Material.Name;
parameters.Grade = nest.Material.Grade;
@@ -73,6 +64,31 @@ public static class MachineCuttingDefaults
return true;
}
/// <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
@@ -112,6 +128,71 @@ public static class MachineCuttingDefaults
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
{
+11
View File
@@ -11,8 +11,19 @@ public class ThicknessConfig
public double Kerf { get; set; }
public string AssistGas { get; set; } = "";
/// <summary>The lead-in of outside cuts (part perimeters), and of inside cuts while <see cref="InsideLeadIn"/> is null.</summary>
public LeadConfig LeadIn { get; set; } = new();
/// <summary>The lead-out of outside cuts, and of inside cuts while <see cref="InsideLeadOut"/> is null.</summary>
public LeadConfig LeadOut { get; set; } = new();
/// <summary>The lead-in of inside cuts (cutouts and holes); null uses <see cref="LeadIn"/>.</summary>
public LeadConfig? InsideLeadIn { get; set; }
/// <summary>The lead-out of inside cuts; null uses <see cref="LeadOut"/>.</summary>
public LeadConfig? InsideLeadOut { get; set; }
public CutOffConfig CutOff { get; set; } = new();
public List<string> PlateSizes { get; set; } = new();
}
@@ -146,6 +146,23 @@ public class MachineCuttingDefaultsTests
Assert.Equal(0.012, Assert.IsType<CleanHoleLeadIn>(parameters.InternalLeadIn).Kerf);
}
[Fact]
public void InsideLeads_GoToInternalContoursOnly()
{
var parameters = new CuttingParameters();
var machine = Machine(LineArc);
var thickness = machine.Materials[0].Thicknesses[0];
thickness.InsideLeadIn = new LeadConfig { Type = "Clean Hole", Length = 0.1, Radius = 0.05 };
thickness.InsideLeadOut = new LeadConfig { Type = "Arc", Radius = 0.04 };
Assert.True(MachineCuttingDefaults.ApplyTo(parameters, machine, NestOf(0.25)));
Assert.IsType<LineArcLeadIn>(parameters.ExternalLeadIn);
Assert.IsType<LineLeadOut>(parameters.ExternalLeadOut);
Assert.Equal(0.05, Assert.IsType<CleanHoleLeadIn>(parameters.InternalLeadIn).ArcRadius);
Assert.Equal(0.04, Assert.IsType<ArcLeadOut>(parameters.InternalLeadOut).Radius);
}
[Fact]
public void LineLineLeadIn_KeepsTheLeadInsButStillSetsLeadOuts()
{
@@ -0,0 +1,102 @@
using System.Windows.Forms;
using OpenNest.Data;
using OpenNest.Forms;
namespace OpenNest.WinForms.Tests.Forms;
/// <summary>
/// The Outside Cuts and Inside Cuts tabs of a thickness in Machine Configuration: inside cuts
/// follow the outside leads until "Same as outside cuts" is cleared, which gives them a copy.
/// </summary>
public class MachineCutsFormsTests
{
[Fact]
public void InsideCutsTab_GivesHolesTheirOwnLeads()
{
RunSta(() =>
{
var directory = Path.Combine(Path.GetTempPath(), "OpenNestTests", Guid.NewGuid().ToString());
Directory.CreateDirectory(directory);
try
{
var provider = new LocalJsonProvider(directory);
var thickness = new ThicknessConfig
{
Value = 0.25,
LeadIn = new LeadConfig { Type = "Arc", Radius = 0.125 },
LeadOut = new LeadConfig { Type = "None" },
};
provider.SaveMachine(new MachineConfig
{
Name = "Laser",
Materials = { new MaterialConfig { Name = "Mild Steel", Thicknesses = { thickness } } },
});
using var form = new MachineConfigForm(provider);
var node = Descendants(Find<TreeView>(form, "_tree").Nodes).Single(n => n.Tag is ThicknessConfig);
// A hidden tree raises no AfterSelect, so show the thickness as selecting it would.
typeof(MachineConfigForm)
.GetMethod("Tree_AfterSelect", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)!
.Invoke(form, new object[] { null, new TreeViewEventArgs(node) });
var shown = (ThicknessConfig)node.Tag;
var pages = All<TabControl>(form).Single().TabPages.Cast<TabPage>().ToList();
Assert.DoesNotContain(pages, p => p.Text is "Lead In" or "Lead Out");
var outside = pages.Single(p => p.Text == "Outside Cuts");
Assert.DoesNotContain(All<ComboBox>(outside), c => c.Items.Contains("Clean Hole"));
var inside = pages.Single(p => p.Text == "Inside Cuts");
var same = All<CheckBox>(inside).Single(c => c.Text == "Same as outside cuts");
Assert.True(same.Checked);
Assert.All(All<ComboBox>(inside), c => Assert.False(c.Enabled));
Assert.NotNull(All<OpenNest.Controls.LeadPreview>(inside).Single().Parameters);
same.Checked = false;
Assert.Equal("Arc", shown.InsideLeadIn!.Type);
Assert.Equal(0.125, shown.InsideLeadIn.Radius);
Assert.NotSame(shown.LeadIn, shown.InsideLeadIn);
Assert.All(All<ComboBox>(inside), c => Assert.True(c.Enabled));
All<ComboBox>(inside).Single(c => c.Items.Contains("Clean Hole")).SelectedItem = "Clean Hole";
Assert.Equal("Clean Hole", shown.InsideLeadIn.Type);
Assert.Equal("Arc", shown.LeadIn.Type);
// Only the chosen type's fields show: line length and arc radius.
var leadInGrid = All<NumericUpDown>(inside).ToList();
Assert.Equal(2, leadInGrid.Count);
leadInGrid[1].Value = 0.07m;
Assert.Equal(0.07, shown.InsideLeadIn.Radius, 6);
Assert.Equal(0.125, shown.LeadIn.Radius);
var preview = All<OpenNest.Controls.LeadPreview>(inside).Single().Parameters;
Assert.Equal(0.07, Assert.IsType<OpenNest.CNC.CuttingStrategy.CleanHoleLeadIn>(preview.InternalLeadIn).ArcRadius, 6);
same.Checked = true;
Assert.Null(shown.InsideLeadIn);
Assert.Null(shown.InsideLeadOut);
}
finally
{
Directory.Delete(directory, recursive: true);
}
});
}
private static IEnumerable<TreeNode> Descendants(TreeNodeCollection nodes) =>
nodes.Cast<TreeNode>().SelectMany(n => Descendants(n.Nodes).Prepend(n));
private static T Find<T>(Control root, string name) where T : Control =>
Assert.IsType<T>(root.Controls.Find(name, true).Single());
private static IEnumerable<T> All<T>(Control root) where T : Control
{
foreach (Control child in root.Controls)
{
if (child is T match)
yield return match;
foreach (var nested in All<T>(child))
yield return nested;
}
}
private static void RunSta(System.Action action) =>
StaTestThread.Run(action, Timeout.InfiniteTimeSpan, "The STA test did not complete.");
}
+23 -428
View File
@@ -8,24 +8,10 @@ namespace OpenNest.Controls
{
public class CuttingPanel : Panel
{
private static readonly string[] LeadInTypes =
{
"None",
"Line",
"Arc",
"Line + Arc",
"Clean Hole",
"Line + Line",
};
private static readonly string[] LeadOutTypes = { "None", "Line", "Arc" };
// Every field label shares this column so inputs line up across sections.
private const int LabelWidth = 150;
private const int FieldIndent = 16;
private const int InputWidth = 100;
// The lead preview's least height, below the type with the most fields.
private const int PreviewHeight = 110;
private static readonly Color HintColor = SystemColors.GrayText;
@@ -33,18 +19,8 @@ namespace OpenNest.Controls
private readonly ToolTip toolTip = new ToolTip { AutoPopDelay = 15000 };
private readonly TabControl tabControl;
private readonly ComboBox cboExternalLeadIn,
cboExternalLeadOut;
private readonly ComboBox cboInternalLeadIn,
cboInternalLeadOut;
private readonly TableLayoutPanel pnlExternalLeadIn,
pnlExternalLeadOut;
private readonly TableLayoutPanel pnlInternalLeadIn,
pnlInternalLeadOut;
private readonly LeadPreview externalPreview = new LeadPreview { Emphasis = ContourType.External };
private readonly LeadPreview internalPreview = new LeadPreview { Emphasis = ContourType.Internal };
private readonly LeadsEditor externalLeads = new LeadsEditor { Emphasis = ContourType.External };
private readonly LeadsEditor internalLeads = new LeadsEditor { Emphasis = ContourType.Internal };
private readonly CheckBox chkTabsEnabled;
private readonly TableLayoutPanel tabOptions;
@@ -124,22 +100,13 @@ namespace OpenNest.Controls
var tabInternal = new TabPage("Holes") { Padding = new Padding(6), UseVisualStyleBackColor = true };
toolTip.SetToolTip(tabControl, "Outside: each part's perimeter. Holes: inner contours and circles.");
var externalContent = SetupTab(
tabExternal,
externalPreview,
out cboExternalLeadIn,
out pnlExternalLeadIn,
out cboExternalLeadOut,
out pnlExternalLeadOut
);
SetupTab(
tabInternal,
internalPreview,
out cboInternalLeadIn,
out pnlInternalLeadIn,
out cboInternalLeadOut,
out pnlInternalLeadOut
);
foreach (var (tab, leads) in new[] { (tabExternal, externalLeads), (tabInternal, internalLeads) })
{
leads.Dock = DockStyle.Fill;
leads.LeadInChanged += (s, e) => OnParametersChanged();
leads.LeadOutChanged += (s, e) => OnParametersChanged();
tab.Controls.Add(leads);
}
tabControl.Controls.Add(tabExternal);
tabControl.Controls.Add(tabInternal);
@@ -282,9 +249,9 @@ namespace OpenNest.Controls
Controls.Add(layout);
PopulateDropdowns();
FitLeadTabs(externalContent);
WireChangeEvents();
// Sized for the type with the most fields, so picking one never moves the sections below;
// the tab strip, page padding and borders take the rest.
tabControl.Height = externalLeads.GetTallestHeight() + Font.Height + 28;
UpdatePreviews();
}
@@ -303,10 +270,10 @@ namespace OpenNest.Controls
{
var parameters = loaded == null ? new CuttingParameters()
: CuttingParametersSerializer.Deserialize(CuttingParametersSerializer.Serialize(loaded));
parameters.ExternalLeadIn = BuildLeadIn(cboExternalLeadIn, pnlExternalLeadIn);
parameters.ExternalLeadOut = BuildLeadOut(cboExternalLeadOut, pnlExternalLeadOut);
parameters.InternalLeadIn = BuildLeadIn(cboInternalLeadIn, pnlInternalLeadIn);
parameters.InternalLeadOut = BuildLeadOut(cboInternalLeadOut, pnlInternalLeadOut);
parameters.ExternalLeadIn = externalLeads.LeadIn;
parameters.ExternalLeadOut = externalLeads.LeadOut;
parameters.InternalLeadIn = internalLeads.LeadIn;
parameters.InternalLeadOut = internalLeads.LeadOut;
parameters.TabsEnabled = chkTabsEnabled.Checked && rbTabAll.Checked;
parameters.TabConfig = new NormalTab { Size = (double)nudTabWidth.Value };
parameters.PierceClearance = (double)nudPierceClearance.Value;
@@ -325,10 +292,10 @@ namespace OpenNest.Controls
suppressEvents = true;
loaded = CuttingParametersSerializer.Deserialize(CuttingParametersSerializer.Serialize(p));
LoadLeadIn(cboExternalLeadIn, pnlExternalLeadIn, p.ExternalLeadIn);
LoadLeadOut(cboExternalLeadOut, pnlExternalLeadOut, p.ExternalLeadOut);
LoadLeadIn(cboInternalLeadIn, pnlInternalLeadIn, p.InternalLeadIn);
LoadLeadOut(cboInternalLeadOut, pnlInternalLeadOut, p.InternalLeadOut);
externalLeads.LeadIn = p.ExternalLeadIn;
externalLeads.LeadOut = p.ExternalLeadOut;
internalLeads.LeadIn = p.InternalLeadIn;
internalLeads.LeadOut = p.InternalLeadOut;
chkTabsEnabled.Checked = p.TabsEnabled || p.SupportTabs;
rbSupportTab.Checked = p.SupportTabs;
@@ -353,8 +320,8 @@ namespace OpenNest.Controls
private void UpdatePreviews()
{
var parameters = BuildParameters();
externalPreview.Parameters = parameters;
internalPreview.Parameters = parameters;
externalLeads.PreviewParameters = parameters;
internalLeads.PreviewParameters = parameters;
}
private void UpdateSupportTabOptions()
@@ -375,225 +342,6 @@ namespace OpenNest.Controls
ParametersChanged?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Sizes the lead tabs for the lead-in and lead-out with the most fields, so picking a
/// type never clips its fields or moves the sections below.
/// </summary>
private void FitLeadTabs(Control content)
{
var tallestLeadIn = 0;
for (var i = 0; i < LeadInTypes.Length; i++)
{
BuildLeadInParamControls(pnlExternalLeadIn, i);
tallestLeadIn = System.Math.Max(tallestLeadIn, pnlExternalLeadIn.GetPreferredSize(Size.Empty).Height);
}
var tallestLeadOut = 0;
for (var i = 0; i < LeadOutTypes.Length; i++)
{
BuildLeadOutParamControls(pnlExternalLeadOut, i);
tallestLeadOut = System.Math.Max(tallestLeadOut, pnlExternalLeadOut.GetPreferredSize(Size.Empty).Height);
}
BuildLeadInParamControls(pnlExternalLeadIn, 0);
BuildLeadOutParamControls(pnlExternalLeadOut, 0);
var contentHeight = content.GetPreferredSize(Size.Empty).Height + tallestLeadIn + tallestLeadOut
+ PreviewHeight;
// Tab strip, page padding and borders.
tabControl.Height = contentHeight + Font.Height + 28;
}
private TableLayoutPanel SetupTab(
TabPage tab,
LeadPreview preview,
out ComboBox leadInCombo,
out TableLayoutPanel leadInPanel,
out ComboBox leadOutCombo,
out TableLayoutPanel leadOutPanel
)
{
var content = new TableLayoutPanel
{
Dock = DockStyle.Top,
AutoSize = true,
AutoSizeMode = AutoSizeMode.GrowAndShrink,
ColumnCount = 1,
};
leadInCombo = CreateCombo();
var leadInType = CreateGrid(0);
AddRow(leadInType, "Lead-in:", leadInCombo, 0, bold: true);
content.Controls.Add(leadInType);
leadInPanel = CreateGrid(FieldIndent);
content.Controls.Add(leadInPanel);
leadOutCombo = CreateCombo();
var leadOutType = CreateGrid(0);
leadOutType.Margin = new Padding(0, 8, 0, 0);
AddRow(leadOutType, "Lead-out:", leadOutCombo, 0, bold: true);
content.Controls.Add(leadOutType);
leadOutPanel = CreateGrid(FieldIndent);
content.Controls.Add(leadOutPanel);
// The preview fills whatever the fields leave, so it grows for types with fewer.
preview.Dock = DockStyle.Fill;
preview.Margin = new Padding(0, 6, 0, 0);
tab.Controls.Add(preview);
tab.Controls.Add(content);
preview.BringToFront();
return content;
}
private void PopulateDropdowns()
{
foreach (
var combo in new[] { cboExternalLeadIn, cboInternalLeadIn }
)
{
combo.Items.AddRange(LeadInTypes);
combo.SelectedIndex = 0;
}
foreach (
var combo in new[] { cboExternalLeadOut, cboInternalLeadOut }
)
{
combo.Items.AddRange(LeadOutTypes);
combo.SelectedIndex = 0;
}
}
private void WireChangeEvents()
{
cboExternalLeadIn.SelectedIndexChanged += OnLeadInTypeChanged;
cboInternalLeadIn.SelectedIndexChanged += OnLeadInTypeChanged;
cboExternalLeadOut.SelectedIndexChanged += OnLeadOutTypeChanged;
cboInternalLeadOut.SelectedIndexChanged += OnLeadOutTypeChanged;
}
private void OnLeadInTypeChanged(object sender, EventArgs e)
{
var combo = (ComboBox)sender;
var panel = GetLeadInPanel(combo);
if (panel != null)
BuildLeadInParamControls(panel, combo.SelectedIndex);
OnParametersChanged();
}
private void OnLeadOutTypeChanged(object sender, EventArgs e)
{
var combo = (ComboBox)sender;
var panel = GetLeadOutPanel(combo);
if (panel != null)
BuildLeadOutParamControls(panel, combo.SelectedIndex);
OnParametersChanged();
}
private TableLayoutPanel GetLeadInPanel(ComboBox combo)
{
if (combo == cboExternalLeadIn)
return pnlExternalLeadIn;
if (combo == cboInternalLeadIn)
return pnlInternalLeadIn;
return null;
}
private TableLayoutPanel GetLeadOutPanel(ComboBox combo)
{
if (combo == cboExternalLeadOut)
return pnlExternalLeadOut;
if (combo == cboInternalLeadOut)
return pnlInternalLeadOut;
return null;
}
private void BuildLeadInParamControls(TableLayoutPanel panel, int typeIndex)
{
ClearRows(panel);
switch (typeIndex)
{
case 1:
AddNumericField(panel, "Length:", 0.25, "Length");
AddAngleField(panel, "Approach angle:", 90, "ApproachAngle");
break;
case 2:
AddNumericField(panel, "Radius:", 0.25, "Radius");
// Degrees of arc: 90 is a quarter circle, 180 a half circle.
AddAngleField(panel, "Sweep angle:", ArcLeadIn.DefaultSweepAngle, "SweepAngle",
increment: 15, minimum: 1, maximum: 180);
break;
case 3:
AddNumericField(panel, "Line length:", 0.25, "LineLength");
AddNumericField(panel, "Arc radius:", 0.125, "ArcRadius");
AddAngleField(panel, "Approach angle:", 135, "ApproachAngle");
break;
case 4:
AddNumericField(panel, "Line length:", 0.25, "LineLength");
AddNumericField(panel, "Arc radius:", 0.125, "ArcRadius");
AddNumericField(panel, "Kerf:", 0.06, "Kerf");
break;
case 5:
AddNumericField(panel, "First length:", 0.25, "Length1");
AddAngleField(panel, "First angle:", 90, "Angle1");
AddNumericField(panel, "Second length:", 0.25, "Length2");
AddAngleField(panel, "Second angle:", 90, "Angle2");
break;
}
}
private void BuildLeadOutParamControls(TableLayoutPanel panel, int typeIndex)
{
ClearRows(panel);
switch (typeIndex)
{
case 1:
AddNumericField(panel, "Length:", 0.25, "Length");
AddAngleField(panel, "Approach angle:", 90, "ApproachAngle");
break;
case 2:
AddNumericField(panel, "Radius:", 0.25, "Radius");
break;
}
}
private void AddAngleField(
TableLayoutPanel panel,
string label,
double defaultValue,
string tag,
double increment = 5,
double minimum = 0,
double maximum = 360
) => AddNumericField(panel, label, defaultValue, tag, increment, minimum, maximum, "°", 2);
private void AddNumericField(
TableLayoutPanel panel,
string label,
double defaultValue,
string tag,
double increment = 0.0625,
double minimum = 0,
double maximum = 9999,
string suffix = null,
int decimalPlaces = 4
)
{
var nud = CreateNumeric(defaultValue, increment);
nud.Minimum = (decimal)minimum;
nud.Maximum = (decimal)maximum;
nud.DecimalPlaces = decimalPlaces;
nud.Value = System.Math.Clamp((decimal)defaultValue, nud.Minimum, nud.Maximum);
nud.Tag = tag;
nud.ValueChanged += (s, e) => OnParametersChanged();
AddRow(panel, label, nud, FieldIndent, suffix);
}
private void AddSection(string title)
{
var header = new Label
@@ -633,8 +381,7 @@ namespace OpenNest.Controls
string label,
Control input,
int indent,
string hint = null,
bool bold = false
string hint = null
)
{
var row = NextRow(grid);
@@ -645,8 +392,6 @@ namespace OpenNest.Controls
Anchor = AnchorStyles.Left,
Margin = new Padding(0, 0, 4, 0),
};
if (bold)
text.Font = new Font(text.Font, FontStyle.Bold);
grid.Controls.Add(text, 0, row);
input.Anchor = AnchorStyles.Left;
@@ -672,16 +417,6 @@ namespace OpenNest.Controls
return row;
}
private static void ClearRows(TableLayoutPanel grid)
{
grid.SuspendLayout();
while (grid.Controls.Count > 0)
grid.Controls[0].Dispose();
grid.RowStyles.Clear();
grid.RowCount = 0;
grid.ResumeLayout();
}
private static FlowLayoutPanel CreateInline(params Control[] controls)
{
var flow = new FlowLayoutPanel
@@ -709,13 +444,6 @@ namespace OpenNest.Controls
Margin = margin ?? new Padding(2, 0, 0, 0),
};
private static ComboBox CreateCombo() =>
new ComboBox
{
DropDownStyle = ComboBoxStyle.DropDownList,
Width = 150,
};
private static NumericUpDown CreateNumeric(double defaultValue, double increment)
{
return new NumericUpDown
@@ -729,138 +457,5 @@ namespace OpenNest.Controls
TextAlign = HorizontalAlignment.Right,
};
}
private static void LoadLeadIn(ComboBox combo, Panel panel, LeadIn leadIn)
{
switch (leadIn)
{
case LineLeadIn line:
combo.SelectedIndex = 1;
SetParam(panel, "Length", line.Length);
SetParam(panel, "ApproachAngle", line.ApproachAngle);
break;
case ArcLeadIn arc:
combo.SelectedIndex = 2;
SetParam(panel, "Radius", arc.Radius);
SetParam(panel, "SweepAngle", arc.SweepAngle);
break;
case LineArcLeadIn lineArc:
combo.SelectedIndex = 3;
SetParam(panel, "LineLength", lineArc.LineLength);
SetParam(panel, "ArcRadius", lineArc.ArcRadius);
SetParam(panel, "ApproachAngle", lineArc.ApproachAngle);
break;
case CleanHoleLeadIn cleanHole:
combo.SelectedIndex = 4;
SetParam(panel, "LineLength", cleanHole.LineLength);
SetParam(panel, "ArcRadius", cleanHole.ArcRadius);
SetParam(panel, "Kerf", cleanHole.Kerf);
break;
case LineLineLeadIn lineLine:
combo.SelectedIndex = 5;
SetParam(panel, "Length1", lineLine.Length1);
SetParam(panel, "Angle1", lineLine.ApproachAngle1);
SetParam(panel, "Length2", lineLine.Length2);
SetParam(panel, "Angle2", lineLine.ApproachAngle2);
break;
default:
combo.SelectedIndex = 0;
break;
}
}
private static void LoadLeadOut(ComboBox combo, Panel panel, LeadOut leadOut)
{
switch (leadOut)
{
case LineLeadOut line:
combo.SelectedIndex = 1;
SetParam(panel, "Length", line.Length);
SetParam(panel, "ApproachAngle", line.ApproachAngle);
break;
case ArcLeadOut arc:
combo.SelectedIndex = 2;
SetParam(panel, "Radius", arc.Radius);
break;
default:
combo.SelectedIndex = 0;
break;
}
}
private static LeadIn BuildLeadIn(ComboBox combo, Panel panel)
{
return combo.SelectedIndex switch
{
1 => new LineLeadIn
{
Length = GetParam(panel, "Length", 0.25),
ApproachAngle = GetParam(panel, "ApproachAngle", 90),
},
2 => new ArcLeadIn
{
Radius = GetParam(panel, "Radius", 0.25),
SweepAngle = GetParam(panel, "SweepAngle", ArcLeadIn.DefaultSweepAngle),
},
3 => new LineArcLeadIn
{
LineLength = GetParam(panel, "LineLength", 0.25),
ArcRadius = GetParam(panel, "ArcRadius", 0.125),
ApproachAngle = GetParam(panel, "ApproachAngle", 135),
},
4 => new CleanHoleLeadIn
{
LineLength = GetParam(panel, "LineLength", 0.25),
ArcRadius = GetParam(panel, "ArcRadius", 0.125),
Kerf = GetParam(panel, "Kerf", 0.06),
},
5 => new LineLineLeadIn
{
Length1 = GetParam(panel, "Length1", 0.25),
ApproachAngle1 = GetParam(panel, "Angle1", 90),
Length2 = GetParam(panel, "Length2", 0.25),
ApproachAngle2 = GetParam(panel, "Angle2", 90),
},
_ => new NoLeadIn(),
};
}
private static LeadOut BuildLeadOut(ComboBox combo, Panel panel)
{
return combo.SelectedIndex switch
{
1 => new LineLeadOut
{
Length = GetParam(panel, "Length", 0.25),
ApproachAngle = GetParam(panel, "ApproachAngle", 90),
},
2 => new ArcLeadOut { Radius = GetParam(panel, "Radius", 0.25) },
_ => new NoLeadOut(),
};
}
private static void SetParam(Panel panel, string tag, double value)
{
foreach (Control c in panel.Controls)
{
if (c is NumericUpDown nud && (string)nud.Tag == tag)
{
nud.Value = double.IsFinite(value)
? System.Math.Clamp((decimal)value, nud.Minimum, nud.Maximum)
: nud.Minimum;
return;
}
}
}
private static double GetParam(Panel panel, string tag, double defaultValue)
{
foreach (Control c in panel.Controls)
{
if (c is NumericUpDown nud && (string)nud.Tag == tag)
return (double)nud.Value;
}
return defaultValue;
}
}
}
+579
View File
@@ -0,0 +1,579 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
using OpenNest.CNC.CuttingStrategy;
namespace OpenNest.Controls
{
/// <summary>
/// Edits one contour type's lead-in and lead-out: a type list for each, the chosen type's
/// fields below it, and a <see cref="LeadPreview"/> filling the rest. Cutting Settings and
/// Machine Configuration both use it; the options hide what a host cannot hold.
/// </summary>
public class LeadsEditor : UserControl
{
private const string None = "None";
private const string Line = "Line";
private const string Arc = "Arc";
private const string LineArc = "Line + Arc";
private const string CleanHole = "Clean Hole";
private const string LineLine = "Line + Line";
private static readonly string[] AllLeadInTypes = { None, Line, Arc, LineArc, CleanHole, LineLine };
private static readonly string[] LeadOutTypes = { None, Line, Arc };
// Every field label shares this column so inputs line up across hosts.
private const int LabelWidth = 150;
private const int FieldIndent = 16;
private const int InputWidth = 100;
// The preview's least height, below the type with the most fields.
private const int PreviewHeight = 110;
private static readonly Color HintColor = SystemColors.GrayText;
private readonly TableLayoutPanel content;
private readonly ComboBox leadInCombo;
private readonly ComboBox leadOutCombo;
private readonly TableLayoutPanel leadInFields;
private readonly TableLayoutPanel leadOutFields;
private readonly LeadPreview preview = new();
private bool allowCleanHole = true;
private bool allowLineLine = true;
private bool showArcSweep = true;
private bool showCleanHoleKerf = true;
private string lengthUnit;
private bool loading;
/// <summary>Raised when the user changes the lead-in's type or fields.</summary>
public event EventHandler LeadInChanged;
/// <summary>Raised when the user changes the lead-out's type or fields.</summary>
public event EventHandler LeadOutChanged;
public LeadsEditor()
{
BackColor = Color.White;
content = new TableLayoutPanel
{
Dock = DockStyle.Top,
AutoSize = true,
AutoSizeMode = AutoSizeMode.GrowAndShrink,
ColumnCount = 1,
};
leadInCombo = CreateCombo();
var leadInType = CreateGrid(0);
AddRow(leadInType, "Lead-in:", leadInCombo, 0, bold: true);
content.Controls.Add(leadInType);
leadInFields = CreateGrid(FieldIndent);
content.Controls.Add(leadInFields);
leadOutCombo = CreateCombo();
var leadOutType = CreateGrid(0);
leadOutType.Margin = new Padding(0, 8, 0, 0);
AddRow(leadOutType, "Lead-out:", leadOutCombo, 0, bold: true);
content.Controls.Add(leadOutType);
leadOutFields = CreateGrid(FieldIndent);
content.Controls.Add(leadOutFields);
// The preview fills whatever the fields leave, so it grows for types with fewer.
preview.Dock = DockStyle.Fill;
preview.Margin = new Padding(0, 6, 0, 0);
Controls.Add(preview);
Controls.Add(content);
preview.BringToFront();
FillCombo(leadInCombo, LeadInTypes(), None);
FillCombo(leadOutCombo, LeadOutTypes, None);
leadInCombo.SelectedIndexChanged += (s, e) =>
{
BuildLeadInFields();
if (!loading)
LeadInChanged?.Invoke(this, EventArgs.Empty);
};
leadOutCombo.SelectedIndexChanged += (s, e) =>
{
BuildLeadOutFields();
if (!loading)
LeadOutChanged?.Invoke(this, EventArgs.Empty);
};
}
/// <summary>The contour the preview draws strong; the other one's leads are faint.</summary>
[DefaultValue(ContourType.External)]
public ContourType Emphasis
{
get => preview.Emphasis;
set
{
preview.Emphasis = value;
preview.Invalidate();
}
}
/// <summary>Whether Clean Hole is offered; a lead already of that type still shows.</summary>
[DefaultValue(true)]
public bool AllowCleanHole
{
get => allowCleanHole;
set
{
allowCleanHole = value;
ReloadLeadInTypes();
}
}
/// <summary>Whether Line + Line is offered; a lead already of that type still shows.</summary>
[DefaultValue(true)]
public bool AllowLineLine
{
get => allowLineLine;
set
{
allowLineLine = value;
ReloadLeadInTypes();
}
}
/// <summary>Whether an arc lead-in shows its sweep angle; hidden, it is the default half circle.</summary>
[DefaultValue(true)]
public bool ShowArcSweep
{
get => showArcSweep;
set
{
showArcSweep = value;
RebuildFields();
}
}
/// <summary>Whether a clean-hole lead-in shows its kerf; hidden, the host supplies it.</summary>
[DefaultValue(true)]
public bool ShowCleanHoleKerf
{
get => showCleanHoleKerf;
set
{
showCleanHoleKerf = value;
RebuildFields();
}
}
/// <summary>A unit shown after each length field, or null for none.</summary>
[DefaultValue(null)]
public string LengthUnit
{
get => lengthUnit;
set
{
lengthUnit = value;
RebuildFields();
}
}
/// <summary>The whole plate's leads, which the preview draws.</summary>
[Browsable(false)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public CuttingParameters PreviewParameters
{
get => preview.Parameters;
set => preview.Parameters = value;
}
/// <summary>Whether the type lists and fields take edits; the preview stays.</summary>
[DefaultValue(true)]
public bool FieldsEnabled
{
get => content.Enabled;
set => content.Enabled = value;
}
[Browsable(false)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public LeadIn LeadIn
{
get => (string)leadInCombo.SelectedItem switch
{
Line => new LineLeadIn
{
Length = GetParam(leadInFields, "Length", 0.25),
ApproachAngle = GetParam(leadInFields, "ApproachAngle", 90),
},
Arc => new ArcLeadIn
{
Radius = GetParam(leadInFields, "Radius", 0.25),
SweepAngle = GetParam(leadInFields, "SweepAngle", ArcLeadIn.DefaultSweepAngle),
},
LineArc => new LineArcLeadIn
{
LineLength = GetParam(leadInFields, "LineLength", 0.25),
ArcRadius = GetParam(leadInFields, "ArcRadius", 0.125),
ApproachAngle = GetParam(leadInFields, "ApproachAngle", 135),
},
CleanHole => new CleanHoleLeadIn
{
LineLength = GetParam(leadInFields, "LineLength", 0.25),
ArcRadius = GetParam(leadInFields, "ArcRadius", 0.125),
Kerf = GetParam(leadInFields, "Kerf", 0.06),
},
LineLine => new LineLineLeadIn
{
Length1 = GetParam(leadInFields, "Length1", 0.25),
ApproachAngle1 = GetParam(leadInFields, "Angle1", 90),
Length2 = GetParam(leadInFields, "Length2", 0.25),
ApproachAngle2 = GetParam(leadInFields, "Angle2", 90),
},
_ => new NoLeadIn(),
};
set
{
loading = true;
FillCombo(leadInCombo, LeadInTypes(), TypeOf(value));
switch (value)
{
case LineLeadIn line:
SetParam(leadInFields, "Length", line.Length);
SetParam(leadInFields, "ApproachAngle", line.ApproachAngle);
break;
case ArcLeadIn arc:
SetParam(leadInFields, "Radius", arc.Radius);
SetParam(leadInFields, "SweepAngle", arc.SweepAngle);
break;
case LineArcLeadIn lineArc:
SetParam(leadInFields, "LineLength", lineArc.LineLength);
SetParam(leadInFields, "ArcRadius", lineArc.ArcRadius);
SetParam(leadInFields, "ApproachAngle", lineArc.ApproachAngle);
break;
case CleanHoleLeadIn cleanHole:
SetParam(leadInFields, "LineLength", cleanHole.LineLength);
SetParam(leadInFields, "ArcRadius", cleanHole.ArcRadius);
SetParam(leadInFields, "Kerf", cleanHole.Kerf);
break;
case LineLineLeadIn lineLine:
SetParam(leadInFields, "Length1", lineLine.Length1);
SetParam(leadInFields, "Angle1", lineLine.ApproachAngle1);
SetParam(leadInFields, "Length2", lineLine.Length2);
SetParam(leadInFields, "Angle2", lineLine.ApproachAngle2);
break;
}
loading = false;
}
}
[Browsable(false)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public LeadOut LeadOut
{
get => (string)leadOutCombo.SelectedItem switch
{
Line => new LineLeadOut
{
Length = GetParam(leadOutFields, "Length", 0.25),
ApproachAngle = GetParam(leadOutFields, "ApproachAngle", 90),
},
Arc => new ArcLeadOut { Radius = GetParam(leadOutFields, "Radius", 0.25) },
_ => new NoLeadOut(),
};
set
{
loading = true;
FillCombo(leadOutCombo, LeadOutTypes, value switch
{
LineLeadOut => Line,
ArcLeadOut => Arc,
_ => None,
});
switch (value)
{
case LineLeadOut line:
SetParam(leadOutFields, "Length", line.Length);
SetParam(leadOutFields, "ApproachAngle", line.ApproachAngle);
break;
case ArcLeadOut arc:
SetParam(leadOutFields, "Radius", arc.Radius);
break;
}
loading = false;
}
}
/// <summary>
/// The height that fits the lead-in and lead-out with the most fields above the preview's
/// least height, so a host can size for it and picking a type never clips its fields.
/// </summary>
public int GetTallestHeight()
{
var (savedIn, savedOut) = (LeadIn, LeadOut);
loading = true;
var tallestLeadIn = 0;
foreach (var type in leadInCombo.Items.Cast<string>().ToList())
{
leadInCombo.SelectedItem = type;
tallestLeadIn = System.Math.Max(tallestLeadIn, leadInFields.GetPreferredSize(Size.Empty).Height);
}
var tallestLeadOut = 0;
foreach (var type in leadOutCombo.Items.Cast<string>().ToList())
{
leadOutCombo.SelectedItem = type;
tallestLeadOut = System.Math.Max(tallestLeadOut, leadOutFields.GetPreferredSize(Size.Empty).Height);
}
leadInCombo.SelectedItem = None;
leadOutCombo.SelectedItem = None;
var rows = content.GetPreferredSize(Size.Empty).Height;
loading = false;
LeadIn = savedIn;
LeadOut = savedOut;
return rows + tallestLeadIn + tallestLeadOut + PreviewHeight + Padding.Vertical;
}
private IEnumerable<string> LeadInTypes() =>
AllLeadInTypes.Where(t => (allowCleanHole || t != CleanHole) && (allowLineLine || t != LineLine));
// Keeps the values while the fields are laid out again.
private void RebuildFields()
{
var (leadIn, leadOut) = (LeadIn, LeadOut);
loading = true;
BuildLeadInFields();
BuildLeadOutFields();
loading = false;
LeadIn = leadIn;
LeadOut = leadOut;
}
private void ReloadLeadInTypes()
{
var lead = LeadIn;
LeadIn = lead;
}
private static string TypeOf(LeadIn leadIn) => leadIn switch
{
LineLeadIn => Line,
ArcLeadIn => Arc,
LineArcLeadIn => LineArc,
CleanHoleLeadIn => CleanHole,
LineLineLeadIn => LineLine,
_ => None,
};
// A type the list does not offer still shows when it is the lead's.
private static void FillCombo(ComboBox combo, IEnumerable<string> types, string selected)
{
var items = types.ToList();
if (!items.Contains(selected))
items.Add(selected);
if (combo.Items.Cast<string>().SequenceEqual(items))
{
combo.SelectedItem = selected;
return;
}
combo.Items.Clear();
combo.Items.AddRange(items.ToArray());
combo.SelectedItem = selected;
}
private void BuildLeadInFields()
{
ClearRows(leadInFields);
switch ((string)leadInCombo.SelectedItem)
{
case Line:
AddLength(leadInFields, "Length:", 0.25, "Length");
AddAngle(leadInFields, "Approach angle:", 90, "ApproachAngle");
break;
case Arc:
AddLength(leadInFields, "Radius:", 0.25, "Radius");
// Degrees of arc: 90 is a quarter circle, 180 a half circle.
if (showArcSweep)
AddAngle(leadInFields, "Sweep angle:", ArcLeadIn.DefaultSweepAngle, "SweepAngle",
increment: 15, minimum: 1, maximum: 180);
break;
case LineArc:
AddLength(leadInFields, "Line length:", 0.25, "LineLength");
AddLength(leadInFields, "Arc radius:", 0.125, "ArcRadius");
AddAngle(leadInFields, "Approach angle:", 135, "ApproachAngle");
break;
case CleanHole:
AddLength(leadInFields, "Line length:", 0.25, "LineLength");
AddLength(leadInFields, "Arc radius:", 0.125, "ArcRadius");
if (showCleanHoleKerf)
AddLength(leadInFields, "Kerf:", 0.06, "Kerf");
break;
case LineLine:
AddLength(leadInFields, "First length:", 0.25, "Length1");
AddAngle(leadInFields, "First angle:", 90, "Angle1");
AddLength(leadInFields, "Second length:", 0.25, "Length2");
AddAngle(leadInFields, "Second angle:", 90, "Angle2");
break;
}
}
private void BuildLeadOutFields()
{
ClearRows(leadOutFields);
switch ((string)leadOutCombo.SelectedItem)
{
case Line:
AddLength(leadOutFields, "Length:", 0.25, "Length");
AddAngle(leadOutFields, "Approach angle:", 90, "ApproachAngle");
break;
case Arc:
AddLength(leadOutFields, "Radius:", 0.25, "Radius");
break;
}
}
private void AddLength(TableLayoutPanel grid, string label, double defaultValue, string tag) =>
AddNumeric(grid, label, defaultValue, tag, 0.0625, 0, 9999, lengthUnit, 4);
private void AddAngle(
TableLayoutPanel grid,
string label,
double defaultValue,
string tag,
double increment = 5,
double minimum = 0,
double maximum = 360
) => AddNumeric(grid, label, defaultValue, tag, increment, minimum, maximum, "°", 2);
private void AddNumeric(
TableLayoutPanel grid,
string label,
double defaultValue,
string tag,
double increment,
double minimum,
double maximum,
string suffix,
int decimalPlaces
)
{
var nud = new NumericUpDown
{
Width = InputWidth,
DecimalPlaces = decimalPlaces,
Increment = (decimal)increment,
Minimum = (decimal)minimum,
Maximum = (decimal)maximum,
TextAlign = HorizontalAlignment.Right,
Tag = tag,
};
nud.Value = System.Math.Clamp((decimal)defaultValue, nud.Minimum, nud.Maximum);
var leadInField = grid == leadInFields;
nud.ValueChanged += (s, e) =>
{
if (loading)
return;
(leadInField ? LeadInChanged : LeadOutChanged)?.Invoke(this, EventArgs.Empty);
};
AddRow(grid, label, nud, FieldIndent, suffix);
}
private static void SetParam(Panel panel, string tag, double value)
{
foreach (Control c in panel.Controls)
{
if (c is NumericUpDown nud && (string)nud.Tag == tag)
{
nud.Value = double.IsFinite(value)
? System.Math.Clamp((decimal)value, nud.Minimum, nud.Maximum)
: nud.Minimum;
return;
}
}
}
private static double GetParam(Panel panel, string tag, double defaultValue)
{
foreach (Control c in panel.Controls)
{
if (c is NumericUpDown nud && (string)nud.Tag == tag)
return (double)nud.Value;
}
return defaultValue;
}
private static TableLayoutPanel CreateGrid(int indent)
{
var grid = new TableLayoutPanel
{
AutoSize = true,
AutoSizeMode = AutoSizeMode.GrowAndShrink,
ColumnCount = 3,
Margin = new Padding(indent, 0, 0, 0),
};
grid.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, LabelWidth - indent));
grid.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
grid.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
return grid;
}
private static void AddRow(
TableLayoutPanel grid,
string label,
Control input,
int indent,
string hint = null,
bool bold = false
)
{
var row = grid.RowCount;
grid.RowCount = row + 1;
grid.RowStyles.Add(new RowStyle(SizeType.AutoSize));
var text = new Label
{
Text = label,
AutoSize = true,
Anchor = AnchorStyles.Left,
Margin = new Padding(0, 0, 4, 0),
};
if (bold)
text.Font = new Font(text.Font, FontStyle.Bold);
grid.Controls.Add(text, 0, row);
input.Anchor = AnchorStyles.Left;
input.Margin = new Padding(0, 2, 0, 2);
grid.Controls.Add(input, 1, row);
if (hint != null)
grid.Controls.Add(
new Label
{
Text = hint,
AutoSize = true,
ForeColor = HintColor,
Anchor = AnchorStyles.Left,
Margin = new Padding(4, 0, 0, 0),
},
2,
row
);
}
private static void ClearRows(TableLayoutPanel grid)
{
grid.SuspendLayout();
while (grid.Controls.Count > 0)
grid.Controls[0].Dispose();
grid.RowStyles.Clear();
grid.RowCount = 0;
grid.ResumeLayout();
}
private static ComboBox CreateCombo() =>
new ComboBox
{
DropDownStyle = ComboBoxStyle.DropDownList,
Width = 150,
};
}
}
+118 -86
View File
@@ -14,18 +14,6 @@ namespace OpenNest.Forms
{
public partial class MachineConfigForm : Form
{
private static readonly string[] LeadInTypes =
{
"None",
"Line",
"Arc",
"Line + Arc",
"Clean Hole",
"Line + Line",
};
private static readonly string[] LeadOutTypes = { "None", "Line", "Arc" };
private static readonly string[] AssistGasOptions = { "O2", "N2", "Air" };
private static readonly string[] CutOffDirectionOptions = Enum.GetNames<CutDirection>();
@@ -630,12 +618,8 @@ namespace OpenNest.Forms
{
var tabs = new TabControl { Dock = DockStyle.Fill };
tabs.TabPages.Add(CreateTab("General", BuildThicknessGeneralTab(thickness, node)));
tabs.TabPages.Add(
CreateTab("Lead In", BuildLeadTab(thickness.LeadIn, LeadInTypes))
);
tabs.TabPages.Add(
CreateTab("Lead Out", BuildLeadTab(thickness.LeadOut, LeadOutTypes))
);
tabs.TabPages.Add(CreateTab("Outside Cuts", BuildOutsideCutsTab(thickness)));
tabs.TabPages.Add(CreateTab("Inside Cuts", BuildInsideCutsTab(thickness)));
tabs.TabPages.Add(CreateTab("Cut Off", BuildCutOffTab(thickness.CutOff)));
tabs.TabPages.Add(CreateTab("Plate Sizes", BuildPlateSizesTab(thickness)));
RememberTab(tabs, _thicknessTab, t => _thicknessTab = t);
@@ -717,82 +701,130 @@ namespace OpenNest.Forms
return panel;
}
private Control BuildLeadTab(LeadConfig lead, string[] typeOptions)
private Control BuildOutsideCutsTab(ThicknessConfig thickness)
{
var layout = CreateDetailLayout();
var row = 0;
var typeCombo = new ComboBox
var editor = CreateLeadsEditor(CNC.CuttingStrategy.ContourType.External);
// Clean Hole is for holes only, so outside cuts do not offer it.
editor.AllowCleanHole = false;
ShowLeads(editor, thickness, thickness.LeadIn, thickness.LeadOut);
editor.LeadInChanged += (s, e) =>
{
DropDownStyle = ComboBoxStyle.DropDownList,
Width = InputWidth,
thickness.LeadIn = MachineCuttingDefaults.ToConfig(editor.LeadIn, thickness.LeadIn);
LeadsEdited(editor, thickness);
};
typeCombo.Items.AddRange(typeOptions);
typeCombo.SelectedItem = typeOptions.Contains(lead.Type) ? lead.Type : typeOptions[0];
typeCombo.SelectedIndexChanged += (s, e) =>
editor.LeadOutChanged += (s, e) =>
{
lead.Type = typeCombo.SelectedItem?.ToString() ?? typeOptions[0];
MarkDirty();
thickness.LeadOut = MachineCuttingDefaults.ToConfig(editor.LeadOut, thickness.LeadOut);
LeadsEdited(editor, thickness);
};
editor.VisibleChanged += (s, e) =>
{
if (editor.Visible)
ShowLeads(editor, thickness, thickness.LeadIn, thickness.LeadOut);
};
AddField(layout, ref row, "Type:", typeCombo);
AddField(
layout,
ref row,
"Length:",
CreateLabeledNumeric(
lead.Length,
v =>
{
lead.Length = v;
MarkDirty();
},
4,
LengthUnitSuffix,
50,
0.001m
)
);
AddField(
layout,
ref row,
"Angle:",
CreateLabeledNumeric(
lead.Angle,
v =>
{
lead.Angle = v;
MarkDirty();
},
1,
_ => "°",
360,
0.1m
)
);
AddField(
layout,
ref row,
"Radius:",
CreateLabeledNumeric(
lead.Radius,
v =>
{
lead.Radius = v;
MarkDirty();
},
4,
LengthUnitSuffix,
50,
0.001m
)
);
var panel = new Panel { Dock = DockStyle.Fill, Padding = new Padding(10) };
panel.Controls.Add(layout);
return panel;
var page = new Panel { Dock = DockStyle.Fill, BackColor = Color.White };
page.Controls.Add(editor);
page.Controls.Add(CreateTabNote("Part perimeters. Inside cuts use these too unless given their own."));
return page;
}
// Inside cuts (cutouts and holes) follow the outside cuts until they get leads of their
// own, which start as a copy of the outside ones.
private Control BuildInsideCutsTab(ThicknessConfig thickness)
{
var editor = CreateLeadsEditor(CNC.CuttingStrategy.ContourType.Internal);
var sameBox = new CheckBox
{
Text = "Same as outside cuts",
AutoSize = true,
Dock = DockStyle.Top,
Padding = new Padding(10, 0, 0, 0),
Checked = thickness.InsideLeadIn == null && thickness.InsideLeadOut == null,
};
void Show()
{
ShowLeads(editor, thickness, thickness.InsideLeadIn ?? thickness.LeadIn,
thickness.InsideLeadOut ?? thickness.LeadOut);
editor.FieldsEnabled = !sameBox.Checked;
}
sameBox.CheckedChanged += (s, e) =>
{
thickness.InsideLeadIn = sameBox.Checked ? null : Copy(thickness.LeadIn);
thickness.InsideLeadOut = sameBox.Checked ? null : Copy(thickness.LeadOut);
MarkDirty();
Show();
};
editor.LeadInChanged += (s, e) =>
{
thickness.InsideLeadIn = MachineCuttingDefaults.ToConfig(editor.LeadIn, thickness.InsideLeadIn);
LeadsEdited(editor, thickness);
};
editor.LeadOutChanged += (s, e) =>
{
thickness.InsideLeadOut = MachineCuttingDefaults.ToConfig(editor.LeadOut, thickness.InsideLeadOut);
LeadsEdited(editor, thickness);
};
// Shown again on each visit, so following the outside cuts shows their current values.
editor.VisibleChanged += (s, e) =>
{
if (editor.Visible)
Show();
};
Show();
var page = new Panel { Dock = DockStyle.Fill, BackColor = Color.White };
page.Controls.Add(editor);
page.Controls.Add(sameBox);
page.Controls.Add(CreateTabNote("Cutouts and holes, full circles included."));
return page;
}
// The machine holds no arc sweep (leads are half circles) and takes a clean hole's kerf
// from the thickness; Line + Line is never applied from it, so it is not offered.
private Controls.LeadsEditor CreateLeadsEditor(CNC.CuttingStrategy.ContourType emphasis)
{
return new Controls.LeadsEditor
{
Dock = DockStyle.Fill,
Padding = new Padding(10, 0, 10, 10),
Emphasis = emphasis,
AllowLineLine = false,
ShowArcSweep = false,
ShowCleanHoleKerf = false,
LengthUnit = LengthUnitSuffix(null),
};
}
private static void ShowLeads(Controls.LeadsEditor editor, ThicknessConfig thickness, LeadConfig leadIn, LeadConfig leadOut)
{
editor.LeadIn = MachineCuttingDefaults.LeadInOf(leadIn, thickness.Kerf) ?? new CNC.CuttingStrategy.NoLeadIn();
editor.LeadOut = MachineCuttingDefaults.LeadOutOf(leadOut);
editor.PreviewParameters = MachineCuttingDefaults.LeadsOf(thickness);
}
private void LeadsEdited(Controls.LeadsEditor editor, ThicknessConfig thickness)
{
editor.PreviewParameters = MachineCuttingDefaults.LeadsOf(thickness);
MarkDirty();
}
private static LeadConfig Copy(LeadConfig lead) =>
new() { Type = lead.Type, Length = lead.Length, Angle = lead.Angle, Radius = lead.Radius };
private static Label CreateTabNote(string text) =>
new()
{
Dock = DockStyle.Top,
AutoSize = true,
MaximumSize = new Size(560, 0),
ForeColor = SystemColors.GrayText,
Padding = new Padding(10, 8, 0, 6),
Text = text,
};
private Control BuildCutOffTab(CutOffConfig cutOff)
{
var layout = CreateDetailLayout();
+9 -4
View File
@@ -27,8 +27,13 @@ soon as its settings dialog is accepted, not only on Apply. A plate that already
parameters keeps them: changing the machine, material or thickness does not rewrite leads
the plate was planned with.
The machine holds one lead-in and one lead-out per thickness; both go to external and internal
contours (internal covers full circles). Types map as follows:
Each thickness holds a lead-in and lead-out for outside cuts (the Outside Cuts tab, external
contours) and, optionally, its own pair for inside cuts (the Inside Cuts tab, internal contours,
full circles included). Inside cuts use the outside pair until "Same as outside cuts" is cleared,
which starts them from a copy of it; configurations saved before the split have no inside pair
and keep applying one pair to both. Both tabs use the shared `LeadsEditor` control that Cutting
Settings uses, without the fields the configuration cannot hold (an arc's sweep; a clean hole's kerf,
which comes from the thickness). Types map as follows:
| Machine type | Lead-in |
|---|---|
@@ -36,8 +41,8 @@ contours (internal covers full circles). Types map as follows:
| Line | line (length, angle) |
| Arc | arc (radius; the default 180-degree half-circle sweep) |
| Line + Arc | line-arc (length, radius, angle) |
| Clean Hole | clean hole (length, radius, the thickness's kerf), holes only: the external lead-in is kept |
| Line + Line | not mapped (the configuration has no second segment): every lead-in is kept |
| Clean Hole | clean hole (length, radius, the thickness's kerf), inside cuts only; an outside Clean Hole (older configurations) keeps the external lead-in |
| Line + Line | not offered; an older configuration's is not mapped (it has no second segment): the lead-in it would set is kept |
Lead-outs map None, Line (length, angle) and Arc (radius); any other type is none.