Files
OpenNest/OpenNest/Controls/CuttingPanel.cs
T
aj 7c3246c6e7 fix(cutting): restrict tabs to external perimeter and clarify tab UI
Tabs were being applied to internal cutouts and circle holes, which is
incorrect — only the external perimeter should be tabbed. Restructured
the Tabs panel to use radio buttons ("Tab all parts" vs "Auto-tab by
smallest dimension") so the two modes are clearly mutually exclusive
instead of the confusing implicit override behavior.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 08:55:30 -04:00

670 lines
24 KiB
C#

using OpenNest.CNC.CuttingStrategy;
using System;
using System.Drawing;
using System.Windows.Forms;
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" };
private readonly TabControl tabControl;
private readonly ComboBox cboExternalLeadIn, cboExternalLeadOut;
private readonly ComboBox cboInternalLeadIn, cboInternalLeadOut;
private readonly ComboBox cboArcCircleLeadIn, cboArcCircleLeadOut;
private readonly Panel pnlExternalLeadIn, pnlExternalLeadOut;
private readonly Panel pnlInternalLeadIn, pnlInternalLeadOut;
private readonly Panel pnlArcCircleLeadIn, pnlArcCircleLeadOut;
private readonly CheckBox chkTabsEnabled;
private readonly NumericUpDown nudTabWidth;
private readonly RadioButton rbTabAll;
private readonly RadioButton rbAutoTab;
private readonly NumericUpDown nudAutoTabMin;
private readonly NumericUpDown nudAutoTabMax;
private readonly NumericUpDown nudPierceClearance;
private readonly CheckBox chkRoundLeadInAngles;
private readonly NumericUpDown nudLeadInAngleIncrement;
private readonly Button btnAutoAssign;
private bool suppressEvents;
public event EventHandler ParametersChanged;
public event EventHandler AutoAssignClicked;
public bool ShowAutoAssign
{
get => btnAutoAssign.Visible;
set => btnAutoAssign.Visible = value;
}
public ContourType? ActiveContourType
{
get
{
return tabControl.SelectedIndex switch
{
0 => ContourType.External,
1 => ContourType.Internal,
2 => ContourType.ArcCircle,
_ => null
};
}
set
{
if (value == null)
return;
var index = value.Value switch
{
ContourType.External => 0,
ContourType.Internal => 1,
ContourType.ArcCircle => 2,
_ => -1
};
if (index >= 0 && tabControl.SelectedIndex != index)
tabControl.SelectedIndex = index;
}
}
public CuttingPanel()
{
AutoScroll = true;
BackColor = Color.White;
// Tab control for contour types — wrapped in a fixed-height panel for Dock.Top
tabControl = new TabControl
{
Dock = DockStyle.Fill
};
var tabExternal = new TabPage("External") { Padding = new Padding(4) };
var tabInternal = new TabPage("Internal") { Padding = new Padding(4) };
var tabArcCircle = new TabPage("Arc / Circle") { Padding = new Padding(4) };
SetupTab(tabExternal, out cboExternalLeadIn, out pnlExternalLeadIn,
out cboExternalLeadOut, out pnlExternalLeadOut);
SetupTab(tabInternal, out cboInternalLeadIn, out pnlInternalLeadIn,
out cboInternalLeadOut, out pnlInternalLeadOut);
SetupTab(tabArcCircle, out cboArcCircleLeadIn, out pnlArcCircleLeadIn,
out cboArcCircleLeadOut, out pnlArcCircleLeadOut);
tabControl.Controls.Add(tabExternal);
tabControl.Controls.Add(tabInternal);
tabControl.Controls.Add(tabArcCircle);
var tabWrapper = new Panel
{
Dock = DockStyle.Top,
Height = 340
};
tabWrapper.Controls.Add(tabControl);
// Tabs section
var tabsPanel = new CollapsiblePanel
{
HeaderText = "Tabs",
Dock = DockStyle.Top,
ExpandedHeight = 160,
IsExpanded = false
};
chkTabsEnabled = new CheckBox
{
Text = "Enable Tabs",
Location = new Point(12, 4),
AutoSize = true
};
tabsPanel.ContentPanel.Controls.Add(chkTabsEnabled);
tabsPanel.ContentPanel.Controls.Add(new Label
{
Text = "Tab Size:",
Location = new Point(160, 6),
AutoSize = true
});
nudTabWidth = CreateNumeric(225, 3, 0.25, 0.0625);
nudTabWidth.Enabled = false;
tabsPanel.ContentPanel.Controls.Add(nudTabWidth);
rbTabAll = new RadioButton
{
Text = "Tab all parts",
Location = new Point(28, 28),
AutoSize = true,
Enabled = false,
Checked = true
};
tabsPanel.ContentPanel.Controls.Add(rbTabAll);
rbAutoTab = new RadioButton
{
Text = "Auto-tab when smallest part dimension is between:",
Location = new Point(28, 50),
AutoSize = true,
Enabled = false
};
tabsPanel.ContentPanel.Controls.Add(rbAutoTab);
tabsPanel.ContentPanel.Controls.Add(new Label
{
Text = "Min:",
Location = new Point(44, 76),
AutoSize = true
});
nudAutoTabMin = CreateNumeric(77, 73, 0, 0.0625);
nudAutoTabMin.Enabled = false;
tabsPanel.ContentPanel.Controls.Add(nudAutoTabMin);
tabsPanel.ContentPanel.Controls.Add(new Label
{
Text = "Max:",
Location = new Point(210, 76),
AutoSize = true
});
nudAutoTabMax = CreateNumeric(245, 73, 0, 0.0625);
nudAutoTabMax.Enabled = false;
tabsPanel.ContentPanel.Controls.Add(nudAutoTabMax);
chkTabsEnabled.CheckedChanged += (s, e) =>
{
var enabled = chkTabsEnabled.Checked;
nudTabWidth.Enabled = enabled;
rbTabAll.Enabled = enabled;
rbAutoTab.Enabled = enabled;
nudAutoTabMin.Enabled = enabled && rbAutoTab.Checked;
nudAutoTabMax.Enabled = enabled && rbAutoTab.Checked;
OnParametersChanged();
};
rbTabAll.CheckedChanged += (s, e) =>
{
nudAutoTabMin.Enabled = chkTabsEnabled.Checked && rbAutoTab.Checked;
nudAutoTabMax.Enabled = chkTabsEnabled.Checked && rbAutoTab.Checked;
OnParametersChanged();
};
// Pierce section
var piercePanel = new CollapsiblePanel
{
HeaderText = "Pierce",
Dock = DockStyle.Top,
ExpandedHeight = 90,
IsExpanded = true
};
piercePanel.ContentPanel.Controls.Add(new Label
{
Text = "Pierce Clearance:",
Location = new Point(12, 6),
AutoSize = true
});
nudPierceClearance = CreateNumeric(130, 3, 0.0625, 0.0625);
piercePanel.ContentPanel.Controls.Add(nudPierceClearance);
chkRoundLeadInAngles = new CheckBox
{
Text = "Round Lead-In Angles",
Location = new Point(12, 32),
AutoSize = true
};
chkRoundLeadInAngles.CheckedChanged += (s, e) =>
{
nudLeadInAngleIncrement.Enabled = chkRoundLeadInAngles.Checked;
OnParametersChanged();
};
piercePanel.ContentPanel.Controls.Add(chkRoundLeadInAngles);
piercePanel.ContentPanel.Controls.Add(new Label
{
Text = "Increment:",
Location = new Point(175, 34),
AutoSize = true
});
nudLeadInAngleIncrement = CreateNumeric(245, 31, 5, 1);
nudLeadInAngleIncrement.DecimalPlaces = 0;
nudLeadInAngleIncrement.Minimum = 1;
nudLeadInAngleIncrement.Maximum = 90;
nudLeadInAngleIncrement.Enabled = false;
nudLeadInAngleIncrement.ValueChanged += (s, e) => OnParametersChanged();
piercePanel.ContentPanel.Controls.Add(nudLeadInAngleIncrement);
// Auto-Assign button — wrapped in a panel for Dock.Top with padding
btnAutoAssign = new Button
{
Text = "Auto-Assign Lead-ins",
Dock = DockStyle.Top,
Height = 32,
Visible = false
};
btnAutoAssign.Click += (s, e) => AutoAssignClicked?.Invoke(this, EventArgs.Empty);
var btnWrapper = new Panel
{
Dock = DockStyle.Top,
Height = 36,
Padding = new Padding(4, 2, 4, 2)
};
btnWrapper.Controls.Add(btnAutoAssign);
// Add in reverse order — Dock.Top stacks top-down
Controls.Add(btnWrapper);
Controls.Add(piercePanel);
Controls.Add(tabsPanel);
Controls.Add(tabWrapper);
// Wire up change events
PopulateDropdowns();
WireChangeEvents();
}
public CuttingParameters BuildParameters()
{
return new CuttingParameters
{
ExternalLeadIn = BuildLeadIn(cboExternalLeadIn, pnlExternalLeadIn),
ExternalLeadOut = BuildLeadOut(cboExternalLeadOut, pnlExternalLeadOut),
InternalLeadIn = BuildLeadIn(cboInternalLeadIn, pnlInternalLeadIn),
InternalLeadOut = BuildLeadOut(cboInternalLeadOut, pnlInternalLeadOut),
ArcCircleLeadIn = BuildLeadIn(cboArcCircleLeadIn, pnlArcCircleLeadIn),
ArcCircleLeadOut = BuildLeadOut(cboArcCircleLeadOut, pnlArcCircleLeadOut),
TabsEnabled = chkTabsEnabled.Checked && rbTabAll.Checked,
TabConfig = new NormalTab { Size = (double)nudTabWidth.Value },
PierceClearance = (double)nudPierceClearance.Value,
RoundLeadInAngles = chkRoundLeadInAngles.Checked,
LeadInAngleIncrement = (double)nudLeadInAngleIncrement.Value,
AutoTabMinSize = chkTabsEnabled.Checked && rbAutoTab.Checked ? (double)nudAutoTabMin.Value : 0,
AutoTabMaxSize = chkTabsEnabled.Checked && rbAutoTab.Checked ? (double)nudAutoTabMax.Value : 0
};
}
public void LoadFromParameters(CuttingParameters p)
{
suppressEvents = true;
LoadLeadIn(cboExternalLeadIn, pnlExternalLeadIn, p.ExternalLeadIn);
LoadLeadOut(cboExternalLeadOut, pnlExternalLeadOut, p.ExternalLeadOut);
LoadLeadIn(cboInternalLeadIn, pnlInternalLeadIn, p.InternalLeadIn);
LoadLeadOut(cboInternalLeadOut, pnlInternalLeadOut, p.InternalLeadOut);
LoadLeadIn(cboArcCircleLeadIn, pnlArcCircleLeadIn, p.ArcCircleLeadIn);
LoadLeadOut(cboArcCircleLeadOut, pnlArcCircleLeadOut, p.ArcCircleLeadOut);
var hasAutoTab = p.AutoTabMinSize > 0 || p.AutoTabMaxSize > 0;
chkTabsEnabled.Checked = p.TabsEnabled || hasAutoTab;
rbAutoTab.Checked = hasAutoTab;
rbTabAll.Checked = !hasAutoTab;
if (p.TabConfig != null)
nudTabWidth.Value = (decimal)p.TabConfig.Size;
nudPierceClearance.Value = (decimal)p.PierceClearance;
chkRoundLeadInAngles.Checked = p.RoundLeadInAngles;
nudLeadInAngleIncrement.Value = (decimal)p.LeadInAngleIncrement;
nudLeadInAngleIncrement.Enabled = p.RoundLeadInAngles;
nudAutoTabMin.Value = (decimal)p.AutoTabMinSize;
nudAutoTabMax.Value = (decimal)p.AutoTabMaxSize;
suppressEvents = false;
}
private void OnParametersChanged()
{
if (!suppressEvents)
ParametersChanged?.Invoke(this, EventArgs.Empty);
}
private static void SetupTab(TabPage tab,
out ComboBox leadInCombo, out Panel leadInPanel,
out ComboBox leadOutCombo, out Panel leadOutPanel)
{
var grpLeadIn = new GroupBox
{
Text = "Lead-In",
Location = new Point(4, 4),
Size = new Size(340, 148)
};
tab.Controls.Add(grpLeadIn);
grpLeadIn.Controls.Add(new Label
{
Text = "Type:",
Location = new Point(8, 22),
AutoSize = true
});
leadInCombo = new ComboBox
{
DropDownStyle = ComboBoxStyle.DropDownList,
Location = new Point(90, 19),
Size = new Size(230, 24)
};
grpLeadIn.Controls.Add(leadInCombo);
leadInPanel = new Panel
{
Location = new Point(8, 48),
Size = new Size(320, 92),
AutoScroll = true
};
grpLeadIn.Controls.Add(leadInPanel);
var grpLeadOut = new GroupBox
{
Text = "Lead-Out",
Location = new Point(4, 156),
Size = new Size(340, 132)
};
tab.Controls.Add(grpLeadOut);
grpLeadOut.Controls.Add(new Label
{
Text = "Type:",
Location = new Point(8, 22),
AutoSize = true
});
leadOutCombo = new ComboBox
{
DropDownStyle = ComboBoxStyle.DropDownList,
Location = new Point(90, 19),
Size = new Size(230, 24)
};
grpLeadOut.Controls.Add(leadOutCombo);
leadOutPanel = new Panel
{
Location = new Point(8, 48),
Size = new Size(320, 76),
AutoScroll = true
};
grpLeadOut.Controls.Add(leadOutPanel);
}
private void PopulateDropdowns()
{
foreach (var combo in new[] { cboExternalLeadIn, cboInternalLeadIn, cboArcCircleLeadIn })
{
combo.Items.AddRange(LeadInTypes);
combo.SelectedIndex = 0;
}
foreach (var combo in new[] { cboExternalLeadOut, cboInternalLeadOut, cboArcCircleLeadOut })
{
combo.Items.AddRange(LeadOutTypes);
combo.SelectedIndex = 0;
}
}
private void WireChangeEvents()
{
cboExternalLeadIn.SelectedIndexChanged += OnLeadInTypeChanged;
cboInternalLeadIn.SelectedIndexChanged += OnLeadInTypeChanged;
cboArcCircleLeadIn.SelectedIndexChanged += OnLeadInTypeChanged;
cboExternalLeadOut.SelectedIndexChanged += OnLeadOutTypeChanged;
cboInternalLeadOut.SelectedIndexChanged += OnLeadOutTypeChanged;
cboArcCircleLeadOut.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 Panel GetLeadInPanel(ComboBox combo)
{
if (combo == cboExternalLeadIn) return pnlExternalLeadIn;
if (combo == cboInternalLeadIn) return pnlInternalLeadIn;
if (combo == cboArcCircleLeadIn) return pnlArcCircleLeadIn;
return null;
}
private Panel GetLeadOutPanel(ComboBox combo)
{
if (combo == cboExternalLeadOut) return pnlExternalLeadOut;
if (combo == cboInternalLeadOut) return pnlInternalLeadOut;
if (combo == cboArcCircleLeadOut) return pnlArcCircleLeadOut;
return null;
}
private void BuildLeadInParamControls(Panel panel, int typeIndex)
{
panel.Controls.Clear();
var y = 0;
switch (typeIndex)
{
case 1:
AddNumericField(panel, "Length:", 0.25, ref y, "Length");
AddNumericField(panel, "Approach Angle:", 90, ref y, "ApproachAngle");
break;
case 2:
AddNumericField(panel, "Radius:", 0.25, ref y, "Radius");
break;
case 3:
AddNumericField(panel, "Line Length:", 0.25, ref y, "LineLength");
AddNumericField(panel, "Arc Radius:", 0.125, ref y, "ArcRadius");
AddNumericField(panel, "Approach Angle:", 135, ref y, "ApproachAngle");
break;
case 4:
AddNumericField(panel, "Line Length:", 0.25, ref y, "LineLength");
AddNumericField(panel, "Arc Radius:", 0.125, ref y, "ArcRadius");
AddNumericField(panel, "Kerf:", 0.06, ref y, "Kerf");
break;
case 5:
AddNumericField(panel, "Length 1:", 0.25, ref y, "Length1");
AddNumericField(panel, "Angle 1:", 90, ref y, "Angle1");
AddNumericField(panel, "Length 2:", 0.25, ref y, "Length2");
AddNumericField(panel, "Angle 2:", 90, ref y, "Angle2");
break;
}
}
private void BuildLeadOutParamControls(Panel panel, int typeIndex)
{
panel.Controls.Clear();
var y = 0;
switch (typeIndex)
{
case 1:
AddNumericField(panel, "Length:", 0.25, ref y, "Length");
AddNumericField(panel, "Approach Angle:", 90, ref y, "ApproachAngle");
break;
case 2:
AddNumericField(panel, "Radius:", 0.25, ref y, "Radius");
break;
}
}
private void AddNumericField(Panel panel, string label, double defaultValue,
ref int y, string tag)
{
panel.Controls.Add(new Label
{
Text = label,
Location = new Point(0, y + 3),
AutoSize = true
});
var nud = CreateNumeric(130, y, defaultValue, 0.0625);
nud.Tag = tag;
nud.ValueChanged += (s, e) => OnParametersChanged();
panel.Controls.Add(nud);
y += 30;
}
private static NumericUpDown CreateNumeric(int x, int y, double defaultValue, double increment)
{
return new NumericUpDown
{
Location = new Point(x, y),
Size = new Size(120, 22),
DecimalPlaces = 4,
Increment = (decimal)increment,
Minimum = 0,
Maximum = 9999,
Value = (decimal)defaultValue
};
}
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);
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)
},
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 = (decimal)value;
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;
}
}
}