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
+87 -41
View File
@@ -31,10 +31,12 @@ namespace OpenNest.Actions
private bool hasSnap; private bool hasSnap;
private SnapType activeSnapType; private SnapType activeSnapType;
private ShapeInfo hoveredContour; private ShapeInfo hoveredContour;
private ShapeInfo lockedContour;
private ContextMenuStrip contextMenu; 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 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 highlightPen = new Pen(Color.Cyan, 2.5f);
private static readonly Pen lockedPen = new Pen(Color.Yellow, 3.0f);
public ActionLeadIn(PlateView plateView) public ActionLeadIn(PlateView plateView)
: base(plateView) : base(plateView)
@@ -48,7 +50,7 @@ namespace OpenNest.Actions
plateView.MouseDown += OnMouseDown; plateView.MouseDown += OnMouseDown;
plateView.KeyDown += OnKeyDown; plateView.KeyDown += OnKeyDown;
plateView.Paint += OnPaint; plateView.Paint += OnPaint;
ShowToolWindow(); ShowSidePanel();
} }
public override void DisconnectEvents() public override void DisconnectEvents()
@@ -58,7 +60,7 @@ namespace OpenNest.Actions
plateView.KeyDown -= OnKeyDown; plateView.KeyDown -= OnKeyDown;
plateView.Paint -= OnPaint; plateView.Paint -= OnPaint;
HideToolWindow(); HideSidePanel();
contextMenu?.Dispose(); contextMenu?.Dispose();
contextMenu = null; contextMenu = null;
@@ -77,18 +79,20 @@ namespace OpenNest.Actions
public override bool IsBusy() => selectedPart != null; public override bool IsBusy() => selectedPart != null;
private void ShowToolWindow() private void ShowSidePanel()
{ {
if (toolWindow == null) var form = plateView.FindForm() as EditNestForm;
{ if (form == null)
toolWindow = new LeadInToolWindow(); return;
toolWindow.AutoAssignClicked += OnAutoAssignClicked;
} cuttingPanel = new CuttingPanel { ShowAutoAssign = true };
cuttingPanel.AutoAssignClicked += OnAutoAssignClicked;
cuttingPanel.ParametersChanged += OnToolParametersChanged;
// Load current parameters or defaults // Load current parameters or defaults
var plate = plateView.Plate; var plate = plateView.Plate;
if (plate?.CuttingParameters != null) if (plate?.CuttingParameters != null)
toolWindow.LoadFromParameters(plate.CuttingParameters); cuttingPanel.LoadFromParameters(plate.CuttingParameters);
else else
{ {
var json = Properties.Settings.Default.CuttingParametersJson; var json = Properties.Settings.Default.CuttingParametersJson;
@@ -97,46 +101,42 @@ namespace OpenNest.Actions
try try
{ {
var saved = CuttingParametersSerializer.Deserialize(json); var saved = CuttingParametersSerializer.Deserialize(json);
toolWindow.LoadFromParameters(saved); cuttingPanel.LoadFromParameters(saved);
} }
catch { /* use defaults */ } catch { /* use defaults */ }
} }
} }
toolWindow.ParametersChanged += OnToolParametersChanged; form.ShowSidePanel(cuttingPanel);
var mainForm = plateView.FindForm();
if (mainForm != null)
toolWindow.Owner = mainForm;
toolWindow.Show();
} }
private void HideToolWindow() private void HideSidePanel()
{ {
if (toolWindow == null) if (cuttingPanel == null)
return; return;
SaveParameters(); SaveParameters();
toolWindow.ParametersChanged -= OnToolParametersChanged; cuttingPanel.ParametersChanged -= OnToolParametersChanged;
toolWindow.AutoAssignClicked -= OnAutoAssignClicked; cuttingPanel.AutoAssignClicked -= OnAutoAssignClicked;
toolWindow.Close();
toolWindow.Dispose(); var form = plateView.FindForm() as EditNestForm;
toolWindow = null; form?.HideSidePanel();
cuttingPanel = null;
} }
private CuttingParameters GetCurrentParameters() private CuttingParameters GetCurrentParameters()
{ {
return toolWindow?.BuildParameters() ?? plateView.Plate?.CuttingParameters ?? new CuttingParameters(); return cuttingPanel?.BuildParameters() ?? plateView.Plate?.CuttingParameters ?? new CuttingParameters();
} }
private void SaveParameters() private void SaveParameters()
{ {
if (toolWindow == null) if (cuttingPanel == null)
return; return;
var parameters = toolWindow.BuildParameters(); var parameters = cuttingPanel.BuildParameters();
var json = CuttingParametersSerializer.Serialize(parameters); var json = CuttingParametersSerializer.Serialize(parameters);
Properties.Settings.Default.CuttingParametersJson = json; Properties.Settings.Default.CuttingParametersJson = json;
Properties.Settings.Default.Save(); Properties.Settings.Default.Save();
@@ -169,7 +169,12 @@ namespace OpenNest.Actions
activeSnapType = SnapType.None; activeSnapType = SnapType.None;
hoveredContour = null; 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 closest = info.Shape.ClosestPointTo(localPt, out var entity);
var dist = closest.DistanceTo(localPt); var dist = closest.DistanceTo(localPt);
@@ -191,9 +196,9 @@ namespace OpenNest.Actions
{ {
TrySnapToEntityPoints(localPt); TrySnapToEntityPoints(localPt);
// Auto-switch tool window tab // Auto-switch tool window tab only when no contour is locked
if (toolWindow != null) if (cuttingPanel != null && lockedContour == null)
toolWindow.ActiveContourType = snapContourType; cuttingPanel.ActiveContourType = snapContourType;
} }
plateView.Invalidate(); plateView.Invalidate();
@@ -208,15 +213,22 @@ namespace OpenNest.Actions
// First click: select a part // First click: select a part
SelectPartAtCursor(); 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(); CommitLeadIn();
} }
} }
else if (e.Button == MouseButtons.Right) else if (e.Button == MouseButtons.Right)
{ {
if (selectedPart != null && selectedPart.HasManualLeadIns) if (lockedContour != null)
UnlockContour();
else if (selectedPart != null && selectedPart.HasManualLeadIns)
ShowContextMenu(e.Location); ShowContextMenu(e.Location);
else else
DeselectPart(); DeselectPart();
@@ -227,7 +239,9 @@ namespace OpenNest.Actions
{ {
if (e.KeyCode == Keys.Escape) if (e.KeyCode == Keys.Escape)
{ {
if (selectedPart != null) if (lockedContour != null)
UnlockContour();
else if (selectedPart != null)
DeselectPart(); DeselectPart();
else else
plateView.SetAction(typeof(ActionSelect)); plateView.SetAction(typeof(ActionSelect));
@@ -243,6 +257,23 @@ namespace OpenNest.Actions
DrawLeadInPreview(g); 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) private void DrawOverlay(Graphics g)
{ {
foreach (var lp in plateView.LayoutParts) foreach (var lp in plateView.LayoutParts)
@@ -254,10 +285,24 @@ namespace OpenNest.Actions
private void DrawHoveredContour(Graphics g) private void DrawHoveredContour(Graphics g)
{ {
if (hoveredContour == null || selectedPart == null) if (selectedPart == null)
return; 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(); using var contourMatrix = new Matrix();
contourMatrix.Translate((float)selectedPart.Location.X, (float)selectedPart.Location.Y); contourMatrix.Translate((float)selectedPart.Location.X, (float)selectedPart.Location.Y);
contourMatrix.Multiply(plateView.Matrix, MatrixOrder.Append); contourMatrix.Multiply(plateView.Matrix, MatrixOrder.Append);
@@ -265,7 +310,7 @@ namespace OpenNest.Actions
var prevSmooth = g.SmoothingMode; var prevSmooth = g.SmoothingMode;
g.SmoothingMode = SmoothingMode.AntiAlias; g.SmoothingMode = SmoothingMode.AntiAlias;
g.DrawPath(highlightPen, contourPath); g.DrawPath(pen, contourPath);
g.SmoothingMode = prevSmooth; g.SmoothingMode = prevSmooth;
} }
@@ -468,7 +513,7 @@ namespace OpenNest.Actions
selectedPart.LeadInsLocked = true; selectedPart.LeadInsLocked = true;
selectedLayoutPart.IsDirty = true; selectedLayoutPart.IsDirty = true;
DeselectPart(); UnlockContour();
plateView.Invalidate(); plateView.Invalidate();
} }
@@ -488,7 +533,7 @@ namespace OpenNest.Actions
selectedPart.LeadInsLocked = true; selectedPart.LeadInsLocked = true;
selectedLayoutPart.IsDirty = true; selectedLayoutPart.IsDirty = true;
DeselectPart(); UnlockContour();
plateView.Invalidate(); plateView.Invalidate();
} }
@@ -515,6 +560,7 @@ namespace OpenNest.Actions
selectedPart = null; selectedPart = null;
profile = null; profile = null;
contours = null; contours = null;
lockedContour = null;
hasSnap = false; hasSnap = false;
activeSnapType = SnapType.None; activeSnapType = SnapType.None;
hoveredContour = null; hoveredContour = null;
+29 -27
View File
@@ -76,14 +76,10 @@ namespace OpenNest.Controls
AutoScroll = true; AutoScroll = true;
BackColor = Color.White; BackColor = Color.White;
var y = 0; // Tab control for contour types — wrapped in a fixed-height panel for Dock.Top
// Tab control for contour types
tabControl = new TabControl tabControl = new TabControl
{ {
Location = new Point(4, y), Dock = DockStyle.Fill
Size = new Size(372, 340),
Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right
}; };
var tabExternal = new TabPage("External") { Padding = new Padding(4) }; var tabExternal = new TabPage("External") { Padding = new Padding(4) };
@@ -101,18 +97,20 @@ namespace OpenNest.Controls
tabControl.Controls.Add(tabInternal); tabControl.Controls.Add(tabInternal);
tabControl.Controls.Add(tabArcCircle); tabControl.Controls.Add(tabArcCircle);
Controls.Add(tabControl); var tabWrapper = new Panel
y += tabControl.Height + 4; {
Dock = DockStyle.Top,
Height = 340
};
tabWrapper.Controls.Add(tabControl);
// Tabs section // Tabs section
var tabsPanel = new CollapsiblePanel var tabsPanel = new CollapsiblePanel
{ {
HeaderText = "Tabs", HeaderText = "Tabs",
Location = new Point(0, y), Dock = DockStyle.Top,
Size = new Size(380, 120),
ExpandedHeight = 120, ExpandedHeight = 120,
IsExpanded = true, IsExpanded = false
Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right
}; };
chkTabsEnabled = new CheckBox chkTabsEnabled = new CheckBox
@@ -159,18 +157,13 @@ namespace OpenNest.Controls
nudAutoTabMax = CreateNumeric(140, 55, 0, 0.0625); nudAutoTabMax = CreateNumeric(140, 55, 0, 0.0625);
tabsPanel.ContentPanel.Controls.Add(nudAutoTabMax); tabsPanel.ContentPanel.Controls.Add(nudAutoTabMax);
Controls.Add(tabsPanel);
y += tabsPanel.Height + 4;
// Pierce section // Pierce section
var piercePanel = new CollapsiblePanel var piercePanel = new CollapsiblePanel
{ {
HeaderText = "Pierce", HeaderText = "Pierce",
Location = new Point(0, y), Dock = DockStyle.Top,
Size = new Size(380, 60),
ExpandedHeight = 60, ExpandedHeight = 60,
IsExpanded = true, IsExpanded = true
Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right
}; };
piercePanel.ContentPanel.Controls.Add(new Label piercePanel.ContentPanel.Controls.Add(new Label
@@ -183,20 +176,29 @@ namespace OpenNest.Controls
nudPierceClearance = CreateNumeric(130, 3, 0.0625, 0.0625); nudPierceClearance = CreateNumeric(130, 3, 0.0625, 0.0625);
piercePanel.ContentPanel.Controls.Add(nudPierceClearance); piercePanel.ContentPanel.Controls.Add(nudPierceClearance);
Controls.Add(piercePanel); // Auto-Assign button — wrapped in a panel for Dock.Top with padding
y += piercePanel.Height + 4;
// Auto-Assign button
btnAutoAssign = new Button btnAutoAssign = new Button
{ {
Text = "Auto-Assign Lead-ins", Text = "Auto-Assign Lead-ins",
Location = new Point(4, y), Dock = DockStyle.Top,
Size = new Size(372, 32), Height = 32,
Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right,
Visible = false Visible = false
}; };
btnAutoAssign.Click += (s, e) => AutoAssignClicked?.Invoke(this, EventArgs.Empty); 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 // Wire up change events
PopulateDropdowns(); PopulateDropdowns();
+42 -1
View File
@@ -37,6 +37,9 @@ namespace OpenNest.Forms
private Button btnNextPlate; private Button btnNextPlate;
private Button btnLastPlate; private Button btnLastPlate;
private SplitContainer viewSplitContainer;
private Panel sidePanel;
/// <summary> /// <summary>
/// Used to distinguish between single/double click on drawing within drawinglistbox. /// 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. /// 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(); InitializeComponent();
CreatePlateHeader(); CreatePlateHeader();
CreateSidePanel();
splitContainer.Panel2.Controls.Add(PlateView); splitContainer.Panel2.Controls.Add(viewSplitContainer);
splitContainer.Panel2.Controls.Add(plateHeaderPanel); splitContainer.Panel2.Controls.Add(plateHeaderPanel);
var renderer = new ToolStripRenderer(ToolbarTheme.Toolbar); var renderer = new ToolStripRenderer(ToolbarTheme.Toolbar);
@@ -146,6 +150,43 @@ namespace OpenNest.Forms
navPanel.Top = (plateHeaderPanel.Height - navPanel.Height) / 2; 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) private static Button CreateNavButton(System.Drawing.Image image)
{ {
return new Button return new Button
-110
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();
}
}
}
}
-24
View File
@@ -238,29 +238,5 @@ namespace OpenNest.Properties {
this["CuttingParametersJson"] = value; 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;
}
}
} }
} }
-6
View File
@@ -56,11 +56,5 @@
<Setting Name="CuttingParametersJson" Type="System.String" Scope="User"> <Setting Name="CuttingParametersJson" Type="System.String" Scope="User">
<Value Profile="(Default)" /> <Value Profile="(Default)" />
</Setting> </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> </Settings>
</SettingsFile> </SettingsFile>