refactor: replace floating tool window with docked side panel

- Add general-purpose ShowSidePanel/HideSidePanel to EditNestForm
- CuttingPanel uses Dock.Top layout so collapsible panels reflow
- Add loop selection step: click contour to lock before placing lead-in
- Stay on selected part after placing a lead-in
- Delete unused LeadInToolWindow

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-02 14:34:20 -04:00
parent 925a1c7751
commit 7a893ef50f
6 changed files with 158 additions and 209 deletions

View File

@@ -31,10 +31,12 @@ namespace OpenNest.Actions
private bool hasSnap;
private SnapType activeSnapType;
private ShapeInfo hoveredContour;
private ShapeInfo lockedContour;
private ContextMenuStrip contextMenu;
private LeadInToolWindow toolWindow;
private CuttingPanel cuttingPanel;
private static readonly Brush grayOverlay = new SolidBrush(Color.FromArgb(160, 180, 180, 180));
private static readonly Pen highlightPen = new Pen(Color.Cyan, 2.5f);
private static readonly Pen lockedPen = new Pen(Color.Yellow, 3.0f);
public ActionLeadIn(PlateView plateView)
: base(plateView)
@@ -48,7 +50,7 @@ namespace OpenNest.Actions
plateView.MouseDown += OnMouseDown;
plateView.KeyDown += OnKeyDown;
plateView.Paint += OnPaint;
ShowToolWindow();
ShowSidePanel();
}
public override void DisconnectEvents()
@@ -58,7 +60,7 @@ namespace OpenNest.Actions
plateView.KeyDown -= OnKeyDown;
plateView.Paint -= OnPaint;
HideToolWindow();
HideSidePanel();
contextMenu?.Dispose();
contextMenu = null;
@@ -77,18 +79,20 @@ namespace OpenNest.Actions
public override bool IsBusy() => selectedPart != null;
private void ShowToolWindow()
private void ShowSidePanel()
{
if (toolWindow == null)
{
toolWindow = new LeadInToolWindow();
toolWindow.AutoAssignClicked += OnAutoAssignClicked;
}
var form = plateView.FindForm() as EditNestForm;
if (form == null)
return;
cuttingPanel = new CuttingPanel { ShowAutoAssign = true };
cuttingPanel.AutoAssignClicked += OnAutoAssignClicked;
cuttingPanel.ParametersChanged += OnToolParametersChanged;
// Load current parameters or defaults
var plate = plateView.Plate;
if (plate?.CuttingParameters != null)
toolWindow.LoadFromParameters(plate.CuttingParameters);
cuttingPanel.LoadFromParameters(plate.CuttingParameters);
else
{
var json = Properties.Settings.Default.CuttingParametersJson;
@@ -97,46 +101,42 @@ namespace OpenNest.Actions
try
{
var saved = CuttingParametersSerializer.Deserialize(json);
toolWindow.LoadFromParameters(saved);
cuttingPanel.LoadFromParameters(saved);
}
catch { /* use defaults */ }
}
}
toolWindow.ParametersChanged += OnToolParametersChanged;
var mainForm = plateView.FindForm();
if (mainForm != null)
toolWindow.Owner = mainForm;
toolWindow.Show();
form.ShowSidePanel(cuttingPanel);
}
private void HideToolWindow()
private void HideSidePanel()
{
if (toolWindow == null)
if (cuttingPanel == null)
return;
SaveParameters();
toolWindow.ParametersChanged -= OnToolParametersChanged;
toolWindow.AutoAssignClicked -= OnAutoAssignClicked;
toolWindow.Close();
toolWindow.Dispose();
toolWindow = null;
cuttingPanel.ParametersChanged -= OnToolParametersChanged;
cuttingPanel.AutoAssignClicked -= OnAutoAssignClicked;
var form = plateView.FindForm() as EditNestForm;
form?.HideSidePanel();
cuttingPanel = null;
}
private CuttingParameters GetCurrentParameters()
{
return toolWindow?.BuildParameters() ?? plateView.Plate?.CuttingParameters ?? new CuttingParameters();
return cuttingPanel?.BuildParameters() ?? plateView.Plate?.CuttingParameters ?? new CuttingParameters();
}
private void SaveParameters()
{
if (toolWindow == null)
if (cuttingPanel == null)
return;
var parameters = toolWindow.BuildParameters();
var parameters = cuttingPanel.BuildParameters();
var json = CuttingParametersSerializer.Serialize(parameters);
Properties.Settings.Default.CuttingParametersJson = json;
Properties.Settings.Default.Save();
@@ -169,7 +169,12 @@ namespace OpenNest.Actions
activeSnapType = SnapType.None;
hoveredContour = null;
foreach (var info in contours)
// When a contour is locked, only snap within that contour
var searchContours = lockedContour != null
? new List<ShapeInfo> { lockedContour }
: contours;
foreach (var info in searchContours)
{
var closest = info.Shape.ClosestPointTo(localPt, out var entity);
var dist = closest.DistanceTo(localPt);
@@ -191,9 +196,9 @@ namespace OpenNest.Actions
{
TrySnapToEntityPoints(localPt);
// Auto-switch tool window tab
if (toolWindow != null)
toolWindow.ActiveContourType = snapContourType;
// Auto-switch tool window tab only when no contour is locked
if (cuttingPanel != null && lockedContour == null)
cuttingPanel.ActiveContourType = snapContourType;
}
plateView.Invalidate();
@@ -208,15 +213,22 @@ namespace OpenNest.Actions
// First click: select a part
SelectPartAtCursor();
}
else if (hasSnap)
else if (lockedContour == null && hasSnap)
{
// Second click: commit lead-in at snap point
// Second click: lock the hovered contour
LockContour(hoveredContour);
}
else if (lockedContour != null && hasSnap)
{
// Third click: commit lead-in at snap point on locked contour
CommitLeadIn();
}
}
else if (e.Button == MouseButtons.Right)
{
if (selectedPart != null && selectedPart.HasManualLeadIns)
if (lockedContour != null)
UnlockContour();
else if (selectedPart != null && selectedPart.HasManualLeadIns)
ShowContextMenu(e.Location);
else
DeselectPart();
@@ -227,7 +239,9 @@ namespace OpenNest.Actions
{
if (e.KeyCode == Keys.Escape)
{
if (selectedPart != null)
if (lockedContour != null)
UnlockContour();
else if (selectedPart != null)
DeselectPart();
else
plateView.SetAction(typeof(ActionSelect));
@@ -243,6 +257,23 @@ namespace OpenNest.Actions
DrawLeadInPreview(g);
}
private void LockContour(ShapeInfo contour)
{
lockedContour = contour;
// Lock the tab to this contour type
if (cuttingPanel != null)
cuttingPanel.ActiveContourType = contour.ContourType;
plateView.Invalidate();
}
private void UnlockContour()
{
lockedContour = null;
plateView.Invalidate();
}
private void DrawOverlay(Graphics g)
{
foreach (var lp in plateView.LayoutParts)
@@ -254,10 +285,24 @@ namespace OpenNest.Actions
private void DrawHoveredContour(Graphics g)
{
if (hoveredContour == null || selectedPart == null)
if (selectedPart == null)
return;
using var contourPath = hoveredContour.Shape.GetGraphicsPath();
// Draw locked contour with distinct pen
if (lockedContour != null)
{
DrawContourHighlight(g, lockedContour.Shape, lockedPen);
return;
}
// Draw hovered contour
if (hoveredContour != null)
DrawContourHighlight(g, hoveredContour.Shape, highlightPen);
}
private void DrawContourHighlight(Graphics g, Shape shape, Pen pen)
{
using var contourPath = shape.GetGraphicsPath();
using var contourMatrix = new Matrix();
contourMatrix.Translate((float)selectedPart.Location.X, (float)selectedPart.Location.Y);
contourMatrix.Multiply(plateView.Matrix, MatrixOrder.Append);
@@ -265,7 +310,7 @@ namespace OpenNest.Actions
var prevSmooth = g.SmoothingMode;
g.SmoothingMode = SmoothingMode.AntiAlias;
g.DrawPath(highlightPen, contourPath);
g.DrawPath(pen, contourPath);
g.SmoothingMode = prevSmooth;
}
@@ -468,7 +513,7 @@ namespace OpenNest.Actions
selectedPart.LeadInsLocked = true;
selectedLayoutPart.IsDirty = true;
DeselectPart();
UnlockContour();
plateView.Invalidate();
}
@@ -488,7 +533,7 @@ namespace OpenNest.Actions
selectedPart.LeadInsLocked = true;
selectedLayoutPart.IsDirty = true;
DeselectPart();
UnlockContour();
plateView.Invalidate();
}
@@ -515,6 +560,7 @@ namespace OpenNest.Actions
selectedPart = null;
profile = null;
contours = null;
lockedContour = null;
hasSnap = false;
activeSnapType = SnapType.None;
hoveredContour = null;

View File

@@ -76,14 +76,10 @@ namespace OpenNest.Controls
AutoScroll = true;
BackColor = Color.White;
var y = 0;
// Tab control for contour types
// Tab control for contour types — wrapped in a fixed-height panel for Dock.Top
tabControl = new TabControl
{
Location = new Point(4, y),
Size = new Size(372, 340),
Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right
Dock = DockStyle.Fill
};
var tabExternal = new TabPage("External") { Padding = new Padding(4) };
@@ -101,18 +97,20 @@ namespace OpenNest.Controls
tabControl.Controls.Add(tabInternal);
tabControl.Controls.Add(tabArcCircle);
Controls.Add(tabControl);
y += tabControl.Height + 4;
var tabWrapper = new Panel
{
Dock = DockStyle.Top,
Height = 340
};
tabWrapper.Controls.Add(tabControl);
// Tabs section
var tabsPanel = new CollapsiblePanel
{
HeaderText = "Tabs",
Location = new Point(0, y),
Size = new Size(380, 120),
Dock = DockStyle.Top,
ExpandedHeight = 120,
IsExpanded = true,
Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right
IsExpanded = false
};
chkTabsEnabled = new CheckBox
@@ -159,18 +157,13 @@ namespace OpenNest.Controls
nudAutoTabMax = CreateNumeric(140, 55, 0, 0.0625);
tabsPanel.ContentPanel.Controls.Add(nudAutoTabMax);
Controls.Add(tabsPanel);
y += tabsPanel.Height + 4;
// Pierce section
var piercePanel = new CollapsiblePanel
{
HeaderText = "Pierce",
Location = new Point(0, y),
Size = new Size(380, 60),
Dock = DockStyle.Top,
ExpandedHeight = 60,
IsExpanded = true,
Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right
IsExpanded = true
};
piercePanel.ContentPanel.Controls.Add(new Label
@@ -183,20 +176,29 @@ namespace OpenNest.Controls
nudPierceClearance = CreateNumeric(130, 3, 0.0625, 0.0625);
piercePanel.ContentPanel.Controls.Add(nudPierceClearance);
Controls.Add(piercePanel);
y += piercePanel.Height + 4;
// Auto-Assign button
// Auto-Assign button — wrapped in a panel for Dock.Top with padding
btnAutoAssign = new Button
{
Text = "Auto-Assign Lead-ins",
Location = new Point(4, y),
Size = new Size(372, 32),
Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right,
Dock = DockStyle.Top,
Height = 32,
Visible = false
};
btnAutoAssign.Click += (s, e) => AutoAssignClicked?.Invoke(this, EventArgs.Empty);
Controls.Add(btnAutoAssign);
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();

View File

@@ -37,6 +37,9 @@ namespace OpenNest.Forms
private Button btnNextPlate;
private Button btnLastPlate;
private SplitContainer viewSplitContainer;
private Panel sidePanel;
/// <summary>
/// Used to distinguish between single/double click on drawing within drawinglistbox.
/// If double click, this is set to false so the single click action won't be triggered.
@@ -53,8 +56,9 @@ namespace OpenNest.Forms
InitializeComponent();
CreatePlateHeader();
CreateSidePanel();
splitContainer.Panel2.Controls.Add(PlateView);
splitContainer.Panel2.Controls.Add(viewSplitContainer);
splitContainer.Panel2.Controls.Add(plateHeaderPanel);
var renderer = new ToolStripRenderer(ToolbarTheme.Toolbar);
@@ -146,6 +150,43 @@ namespace OpenNest.Forms
navPanel.Top = (plateHeaderPanel.Height - navPanel.Height) / 2;
}
private void CreateSidePanel()
{
sidePanel = new Panel
{
Dock = DockStyle.Fill,
AutoScroll = true,
BackColor = Color.White
};
viewSplitContainer = new SplitContainer
{
Dock = DockStyle.Fill,
Orientation = Orientation.Vertical,
FixedPanel = FixedPanel.Panel2,
Panel2MinSize = 0
};
viewSplitContainer.Panel1.Controls.Add(PlateView);
viewSplitContainer.Panel2.Controls.Add(sidePanel);
viewSplitContainer.Panel2Collapsed = true;
}
public void ShowSidePanel(Control content, int width = 390)
{
sidePanel.Controls.Clear();
content.Dock = DockStyle.Fill;
sidePanel.Controls.Add(content);
viewSplitContainer.SplitterDistance = viewSplitContainer.Width - width;
viewSplitContainer.Panel2Collapsed = false;
}
public void HideSidePanel()
{
viewSplitContainer.Panel2Collapsed = true;
sidePanel.Controls.Clear();
}
private static Button CreateNavButton(System.Drawing.Image image)
{
return new Button

View File

@@ -1,110 +0,0 @@
using OpenNest.CNC.CuttingStrategy;
using OpenNest.Controls;
using System;
using System.Drawing;
using System.Windows.Forms;
namespace OpenNest.Forms
{
public class LeadInToolWindow : Form
{
private readonly CuttingPanel cuttingPanel;
public CuttingPanel CuttingPanel => cuttingPanel;
public event EventHandler ParametersChanged
{
add => cuttingPanel.ParametersChanged += value;
remove => cuttingPanel.ParametersChanged -= value;
}
public event EventHandler AutoAssignClicked
{
add => cuttingPanel.AutoAssignClicked += value;
remove => cuttingPanel.AutoAssignClicked -= value;
}
public LeadInToolWindow()
{
Text = "Lead-In Properties";
FormBorderStyle = FormBorderStyle.SizableToolWindow;
ShowInTaskbar = false;
StartPosition = FormStartPosition.Manual;
MinimumSize = new Size(300, 400);
Size = new Size(400, 580);
cuttingPanel = new CuttingPanel
{
Dock = DockStyle.Fill,
ShowAutoAssign = true
};
Controls.Add(cuttingPanel);
RestorePosition();
}
public CuttingParameters BuildParameters() => cuttingPanel.BuildParameters();
public void LoadFromParameters(CuttingParameters p) => cuttingPanel.LoadFromParameters(p);
public ContourType? ActiveContourType
{
get => cuttingPanel.ActiveContourType;
set => cuttingPanel.ActiveContourType = value;
}
protected override void OnFormClosing(FormClosingEventArgs e)
{
if (e.CloseReason == CloseReason.UserClosing)
{
e.Cancel = true;
Hide();
}
SavePosition();
base.OnFormClosing(e);
}
protected override void OnMove(EventArgs e)
{
base.OnMove(e);
if (WindowState == FormWindowState.Normal)
SavePosition();
}
protected override void OnResize(EventArgs e)
{
base.OnResize(e);
if (WindowState == FormWindowState.Normal)
SavePosition();
}
private void SavePosition()
{
if (WindowState != FormWindowState.Normal)
return;
Properties.Settings.Default.LeadInToolWindowLocation = Location;
Properties.Settings.Default.LeadInToolWindowSize = Size;
Properties.Settings.Default.Save();
}
private void RestorePosition()
{
var savedLocation = Properties.Settings.Default.LeadInToolWindowLocation;
var savedSize = Properties.Settings.Default.LeadInToolWindowSize;
if (savedSize.Width > 0 && savedSize.Height > 0)
Size = savedSize;
if (savedLocation.X != 0 || savedLocation.Y != 0)
{
var screen = Screen.FromPoint(savedLocation);
if (screen.WorkingArea.Contains(savedLocation))
Location = savedLocation;
else
CenterToParent();
}
}
}
}

View File

@@ -238,29 +238,5 @@ namespace OpenNest.Properties {
this["CuttingParametersJson"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("0, 0")]
public global::System.Drawing.Point LeadInToolWindowLocation {
get {
return ((global::System.Drawing.Point)(this["LeadInToolWindowLocation"]));
}
set {
this["LeadInToolWindowLocation"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("0, 0")]
public global::System.Drawing.Size LeadInToolWindowSize {
get {
return ((global::System.Drawing.Size)(this["LeadInToolWindowSize"]));
}
set {
this["LeadInToolWindowSize"] = value;
}
}
}
}

View File

@@ -56,11 +56,5 @@
<Setting Name="CuttingParametersJson" Type="System.String" Scope="User">
<Value Profile="(Default)" />
</Setting>
<Setting Name="LeadInToolWindowLocation" Type="System.Drawing.Point" Scope="User">
<Value Profile="(Default)">0, 0</Value>
</Setting>
<Setting Name="LeadInToolWindowSize" Type="System.Drawing.Size" Scope="User">
<Value Profile="(Default)">0, 0</Value>
</Setting>
</Settings>
</SettingsFile>