diff --git a/OpenNest.Data/MachineCuttingDefaults.cs b/OpenNest.Data/MachineCuttingDefaults.cs
index df7fb139..22ff48f6 100644
--- a/OpenNest.Data/MachineCuttingDefaults.cs
+++ b/OpenNest.Data/MachineCuttingDefaults.cs
@@ -3,12 +3,12 @@ using OpenNest.CNC.CuttingStrategy;
namespace OpenNest.Data;
///
-/// 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.
///
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;
}
+ ///
+ /// 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
@@ -112,6 +128,71 @@ public static class MachineCuttingDefaults
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
{
diff --git a/OpenNest.Data/ThicknessConfig.cs b/OpenNest.Data/ThicknessConfig.cs
index 6614615d..6d674e8b 100644
--- a/OpenNest.Data/ThicknessConfig.cs
+++ b/OpenNest.Data/ThicknessConfig.cs
@@ -11,8 +11,19 @@ public class ThicknessConfig
public double Kerf { get; set; }
public string AssistGas { get; set; } = "";
+
+ /// The lead-in of outside cuts (part perimeters), and of inside cuts while is null.
public LeadConfig LeadIn { get; set; } = new();
+
+ /// The lead-out of outside cuts, and of inside cuts while is null.
public LeadConfig LeadOut { get; set; } = new();
+
+ /// The lead-in of inside cuts (cutouts and holes); null uses .
+ public LeadConfig? InsideLeadIn { get; set; }
+
+ /// The lead-out of inside cuts; null uses .
+ public LeadConfig? InsideLeadOut { get; set; }
+
public CutOffConfig CutOff { get; set; } = new();
public List PlateSizes { get; set; } = new();
}
diff --git a/OpenNest.Tests/Data/MachineCuttingDefaultsTests.cs b/OpenNest.Tests/Data/MachineCuttingDefaultsTests.cs
index a6d96fbe..5cb46e29 100644
--- a/OpenNest.Tests/Data/MachineCuttingDefaultsTests.cs
+++ b/OpenNest.Tests/Data/MachineCuttingDefaultsTests.cs
@@ -146,6 +146,23 @@ public class MachineCuttingDefaultsTests
Assert.Equal(0.012, Assert.IsType(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(parameters.ExternalLeadIn);
+ Assert.IsType(parameters.ExternalLeadOut);
+ Assert.Equal(0.05, Assert.IsType(parameters.InternalLeadIn).ArcRadius);
+ Assert.Equal(0.04, Assert.IsType(parameters.InternalLeadOut).Radius);
+ }
+
[Fact]
public void LineLineLeadIn_KeepsTheLeadInsButStillSetsLeadOuts()
{
diff --git a/OpenNest.WinForms.Tests/Forms/MachineCutsFormsTests.cs b/OpenNest.WinForms.Tests/Forms/MachineCutsFormsTests.cs
new file mode 100644
index 00000000..cd3f0e9a
--- /dev/null
+++ b/OpenNest.WinForms.Tests/Forms/MachineCutsFormsTests.cs
@@ -0,0 +1,102 @@
+using System.Windows.Forms;
+using OpenNest.Data;
+using OpenNest.Forms;
+
+namespace OpenNest.WinForms.Tests.Forms;
+
+///
+/// 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.
+///
+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(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(form).Single().TabPages.Cast().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(outside), c => c.Items.Contains("Clean Hole"));
+
+ var inside = pages.Single(p => p.Text == "Inside Cuts");
+ var same = All(inside).Single(c => c.Text == "Same as outside cuts");
+ Assert.True(same.Checked);
+ Assert.All(All(inside), c => Assert.False(c.Enabled));
+ Assert.NotNull(All(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(inside), c => Assert.True(c.Enabled));
+
+ All(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(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(inside).Single().Parameters;
+ Assert.Equal(0.07, Assert.IsType(preview.InternalLeadIn).ArcRadius, 6);
+
+ same.Checked = true;
+ Assert.Null(shown.InsideLeadIn);
+ Assert.Null(shown.InsideLeadOut);
+ }
+ finally
+ {
+ Directory.Delete(directory, recursive: true);
+ }
+ });
+ }
+
+ private static IEnumerable Descendants(TreeNodeCollection nodes) =>
+ nodes.Cast().SelectMany(n => Descendants(n.Nodes).Prepend(n));
+
+ private static T Find(Control root, string name) where T : Control =>
+ Assert.IsType(root.Controls.Find(name, true).Single());
+
+ private static IEnumerable All(Control root) where T : Control
+ {
+ foreach (Control child in root.Controls)
+ {
+ if (child is T match)
+ yield return match;
+ foreach (var nested in All(child))
+ yield return nested;
+ }
+ }
+
+ private static void RunSta(System.Action action) =>
+ StaTestThread.Run(action, Timeout.InfiniteTimeSpan, "The STA test did not complete.");
+}
diff --git a/OpenNest/Controls/CuttingPanel.cs b/OpenNest/Controls/CuttingPanel.cs
index 238d5958..75ce056f 100644
--- a/OpenNest/Controls/CuttingPanel.cs
+++ b/OpenNest/Controls/CuttingPanel.cs
@@ -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);
}
- ///
- /// 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.
- ///
- 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;
- }
}
}
diff --git a/OpenNest/Controls/LeadsEditor.cs b/OpenNest/Controls/LeadsEditor.cs
new file mode 100644
index 00000000..326f6e2a
--- /dev/null
+++ b/OpenNest/Controls/LeadsEditor.cs
@@ -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
+{
+ ///
+ /// Edits one contour type's lead-in and lead-out: a type list for each, the chosen type's
+ /// fields below it, and a filling the rest. Cutting Settings and
+ /// Machine Configuration both use it; the options hide what a host cannot hold.
+ ///
+ 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;
+
+ /// Raised when the user changes the lead-in's type or fields.
+ public event EventHandler LeadInChanged;
+
+ /// Raised when the user changes the lead-out's type or fields.
+ 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);
+ };
+ }
+
+ /// The contour the preview draws strong; the other one's leads are faint.
+ [DefaultValue(ContourType.External)]
+ public ContourType Emphasis
+ {
+ get => preview.Emphasis;
+ set
+ {
+ preview.Emphasis = value;
+ preview.Invalidate();
+ }
+ }
+
+ /// Whether Clean Hole is offered; a lead already of that type still shows.
+ [DefaultValue(true)]
+ public bool AllowCleanHole
+ {
+ get => allowCleanHole;
+ set
+ {
+ allowCleanHole = value;
+ ReloadLeadInTypes();
+ }
+ }
+
+ /// Whether Line + Line is offered; a lead already of that type still shows.
+ [DefaultValue(true)]
+ public bool AllowLineLine
+ {
+ get => allowLineLine;
+ set
+ {
+ allowLineLine = value;
+ ReloadLeadInTypes();
+ }
+ }
+
+ /// Whether an arc lead-in shows its sweep angle; hidden, it is the default half circle.
+ [DefaultValue(true)]
+ public bool ShowArcSweep
+ {
+ get => showArcSweep;
+ set
+ {
+ showArcSweep = value;
+ RebuildFields();
+ }
+ }
+
+ /// Whether a clean-hole lead-in shows its kerf; hidden, the host supplies it.
+ [DefaultValue(true)]
+ public bool ShowCleanHoleKerf
+ {
+ get => showCleanHoleKerf;
+ set
+ {
+ showCleanHoleKerf = value;
+ RebuildFields();
+ }
+ }
+
+ /// A unit shown after each length field, or null for none.
+ [DefaultValue(null)]
+ public string LengthUnit
+ {
+ get => lengthUnit;
+ set
+ {
+ lengthUnit = value;
+ RebuildFields();
+ }
+ }
+
+ /// The whole plate's leads, which the preview draws.
+ [Browsable(false)]
+ [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
+ public CuttingParameters PreviewParameters
+ {
+ get => preview.Parameters;
+ set => preview.Parameters = value;
+ }
+
+ /// Whether the type lists and fields take edits; the preview stays.
+ [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;
+ }
+ }
+
+ ///
+ /// 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.
+ ///
+ public int GetTallestHeight()
+ {
+ var (savedIn, savedOut) = (LeadIn, LeadOut);
+ loading = true;
+
+ var tallestLeadIn = 0;
+ foreach (var type in leadInCombo.Items.Cast().ToList())
+ {
+ leadInCombo.SelectedItem = type;
+ tallestLeadIn = System.Math.Max(tallestLeadIn, leadInFields.GetPreferredSize(Size.Empty).Height);
+ }
+ var tallestLeadOut = 0;
+ foreach (var type in leadOutCombo.Items.Cast().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 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 types, string selected)
+ {
+ var items = types.ToList();
+ if (!items.Contains(selected))
+ items.Add(selected);
+ if (combo.Items.Cast().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,
+ };
+ }
+}
diff --git a/OpenNest/Forms/MachineConfigForm.cs b/OpenNest/Forms/MachineConfigForm.cs
index 37bcd8bb..7e763037 100644
--- a/OpenNest/Forms/MachineConfigForm.cs
+++ b/OpenNest/Forms/MachineConfigForm.cs
@@ -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();
@@ -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();
diff --git a/docs/machine-cutting-defaults.md b/docs/machine-cutting-defaults.md
index 14fce265..6183402d 100644
--- a/docs/machine-cutting-defaults.md
+++ b/docs/machine-cutting-defaults.md
@@ -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.