feat(ui): add remnant viewer tool window
Adds a toolbar button that opens a dockable remnant viewer showing tiered remnants (priority, size, area, location) with color-coded overlay rendering on the plate view. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -33,6 +33,7 @@ namespace OpenNest.Controls
|
||||
private List<LayoutPart> temporaryParts = new List<LayoutPart>();
|
||||
private Point middleMouseDownPoint;
|
||||
private Box activeWorkArea;
|
||||
private List<Box> debugRemnants;
|
||||
|
||||
public Box ActiveWorkArea
|
||||
{
|
||||
@@ -44,6 +45,18 @@ namespace OpenNest.Controls
|
||||
}
|
||||
}
|
||||
|
||||
public List<Box> DebugRemnants
|
||||
{
|
||||
get => debugRemnants;
|
||||
set
|
||||
{
|
||||
debugRemnants = value;
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public List<int> DebugRemnantPriorities { get; set; }
|
||||
|
||||
public List<LayoutPart> SelectedParts;
|
||||
public ReadOnlyCollection<LayoutPart> Parts;
|
||||
|
||||
@@ -374,6 +387,7 @@ namespace OpenNest.Controls
|
||||
DrawPlate(e.Graphics);
|
||||
DrawParts(e.Graphics);
|
||||
DrawActiveWorkArea(e.Graphics);
|
||||
DrawDebugRemnants(e.Graphics);
|
||||
|
||||
base.OnPaint(e);
|
||||
}
|
||||
@@ -632,6 +646,51 @@ namespace OpenNest.Controls
|
||||
g.DrawRectangle(pen, rect.X, rect.Y, rect.Width, rect.Height);
|
||||
}
|
||||
|
||||
// Priority 0 = green (preferred), 1 = yellow (extend), 2 = red (last resort)
|
||||
private static readonly Color[] PriorityFills =
|
||||
{
|
||||
Color.FromArgb(60, Color.LimeGreen),
|
||||
Color.FromArgb(60, Color.Gold),
|
||||
Color.FromArgb(60, Color.Salmon),
|
||||
};
|
||||
|
||||
private static readonly Color[] PriorityBorders =
|
||||
{
|
||||
Color.FromArgb(180, Color.Green),
|
||||
Color.FromArgb(180, Color.DarkGoldenrod),
|
||||
Color.FromArgb(180, Color.DarkRed),
|
||||
};
|
||||
|
||||
private void DrawDebugRemnants(Graphics g)
|
||||
{
|
||||
if (debugRemnants == null || debugRemnants.Count == 0)
|
||||
return;
|
||||
|
||||
for (var i = 0; i < debugRemnants.Count; i++)
|
||||
{
|
||||
var box = debugRemnants[i];
|
||||
var loc = PointWorldToGraph(box.Location);
|
||||
var w = LengthWorldToGui(box.Width);
|
||||
var h = LengthWorldToGui(box.Length);
|
||||
var rect = new RectangleF(loc.X, loc.Y - h, w, h);
|
||||
|
||||
var priority = DebugRemnantPriorities != null && i < DebugRemnantPriorities.Count
|
||||
? System.Math.Min(DebugRemnantPriorities[i], 2)
|
||||
: 0;
|
||||
|
||||
using var brush = new SolidBrush(PriorityFills[priority]);
|
||||
g.FillRectangle(brush, rect);
|
||||
|
||||
using var pen = new Pen(PriorityBorders[priority], 1.5f);
|
||||
g.DrawRectangle(pen, rect.X, rect.Y, rect.Width, rect.Height);
|
||||
|
||||
var label = $"P{priority} {box.Width:F1}x{box.Length:F1}";
|
||||
using var font = new Font("Segoe UI", 8f);
|
||||
using var sf = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center };
|
||||
g.DrawString(label, font, Brushes.Black, rect, sf);
|
||||
}
|
||||
}
|
||||
|
||||
public LayoutPart GetPartAtControlPoint(Point pt)
|
||||
{
|
||||
var pt2 = PointControlToGraph(pt);
|
||||
|
||||
14
OpenNest/Forms/MainForm.Designer.cs
generated
14
OpenNest/Forms/MainForm.Designer.cs
generated
@@ -149,6 +149,7 @@
|
||||
engineLabel = new System.Windows.Forms.ToolStripLabel();
|
||||
engineComboBox = new System.Windows.Forms.ToolStripComboBox();
|
||||
btnAutoNest = new System.Windows.Forms.ToolStripButton();
|
||||
btnShowRemnants = new System.Windows.Forms.ToolStripButton();
|
||||
pEPToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
openNestToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
menuStrip1.SuspendLayout();
|
||||
@@ -888,7 +889,7 @@
|
||||
// toolStrip1
|
||||
//
|
||||
toolStrip1.AutoSize = false;
|
||||
toolStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { btnNew, btnOpen, btnSave, btnSaveAs, toolStripSeparator1, btnFirstPlate, btnPreviousPlate, btnNextPlate, btnLastPlate, toolStripSeparator3, btnZoomOut, btnZoomIn, btnZoomToFit, toolStripSeparator4, engineLabel, engineComboBox, btnAutoNest });
|
||||
toolStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { btnNew, btnOpen, btnSave, btnSaveAs, toolStripSeparator1, btnFirstPlate, btnPreviousPlate, btnNextPlate, btnLastPlate, toolStripSeparator3, btnZoomOut, btnZoomIn, btnZoomToFit, toolStripSeparator4, engineLabel, engineComboBox, btnAutoNest, btnShowRemnants });
|
||||
toolStrip1.Location = new System.Drawing.Point(0, 24);
|
||||
toolStrip1.Name = "toolStrip1";
|
||||
toolStrip1.Size = new System.Drawing.Size(1281, 40);
|
||||
@@ -1067,7 +1068,15 @@
|
||||
btnAutoNest.Size = new System.Drawing.Size(64, 37);
|
||||
btnAutoNest.Text = "Auto Nest";
|
||||
btnAutoNest.Click += RunAutoNest_Click;
|
||||
//
|
||||
//
|
||||
// btnShowRemnants
|
||||
//
|
||||
btnShowRemnants.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
|
||||
btnShowRemnants.Name = "btnShowRemnants";
|
||||
btnShowRemnants.Size = new System.Drawing.Size(64, 37);
|
||||
btnShowRemnants.Text = "Remnants";
|
||||
btnShowRemnants.Click += ShowRemnants_Click;
|
||||
//
|
||||
// pEPToolStripMenuItem
|
||||
//
|
||||
pEPToolStripMenuItem.Name = "pEPToolStripMenuItem";
|
||||
@@ -1232,5 +1241,6 @@
|
||||
private System.Windows.Forms.ToolStripLabel engineLabel;
|
||||
private System.Windows.Forms.ToolStripComboBox engineComboBox;
|
||||
private System.Windows.Forms.ToolStripButton btnAutoNest;
|
||||
private System.Windows.Forms.ToolStripButton btnShowRemnants;
|
||||
}
|
||||
}
|
||||
@@ -737,6 +737,49 @@ namespace OpenNest.Forms
|
||||
activeForm.LoadNextPlate();
|
||||
}
|
||||
|
||||
private RemnantViewerForm remnantViewer;
|
||||
|
||||
private void ShowRemnants_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (activeForm?.PlateView?.Plate == null)
|
||||
return;
|
||||
|
||||
var plate = activeForm.PlateView.Plate;
|
||||
|
||||
// Minimum remnant dimension = smallest part bbox dimension on the plate.
|
||||
var minDim = 0.0;
|
||||
var nest = activeForm.Nest;
|
||||
if (nest != null)
|
||||
{
|
||||
foreach (var drawing in nest.Drawings)
|
||||
{
|
||||
var bbox = drawing.Program.BoundingBox();
|
||||
var dim = System.Math.Min(bbox.Width, bbox.Length);
|
||||
if (minDim == 0 || dim < minDim)
|
||||
minDim = dim;
|
||||
}
|
||||
}
|
||||
|
||||
var finder = RemnantFinder.FromPlate(plate);
|
||||
var tiered = finder.FindTieredRemnants(minDim);
|
||||
|
||||
if (remnantViewer == null || remnantViewer.IsDisposed)
|
||||
{
|
||||
remnantViewer = new RemnantViewerForm();
|
||||
remnantViewer.Owner = this;
|
||||
|
||||
// Position next to the main form's right edge.
|
||||
var screen = Screen.FromControl(this);
|
||||
remnantViewer.Location = new Point(
|
||||
System.Math.Min(Right, screen.WorkingArea.Right - remnantViewer.Width),
|
||||
Top);
|
||||
}
|
||||
|
||||
remnantViewer.LoadRemnants(tiered, activeForm.PlateView);
|
||||
remnantViewer.Show();
|
||||
remnantViewer.BringToFront();
|
||||
}
|
||||
|
||||
private async void RunAutoNest_Click(object sender, EventArgs e)
|
||||
{
|
||||
var form = new AutoNestForm(activeForm.Nest);
|
||||
@@ -744,7 +787,7 @@ namespace OpenNest.Forms
|
||||
|
||||
if (form.ShowDialog() != System.Windows.Forms.DialogResult.OK)
|
||||
return;
|
||||
|
||||
|
||||
var items = form.GetNestItems();
|
||||
|
||||
if (!items.Any(it => it.Quantity > 0))
|
||||
|
||||
119
OpenNest/Forms/RemnantViewerForm.cs
Normal file
119
OpenNest/Forms/RemnantViewerForm.cs
Normal file
@@ -0,0 +1,119 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Windows.Forms;
|
||||
using OpenNest.Controls;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Forms
|
||||
{
|
||||
public class RemnantViewerForm : Form
|
||||
{
|
||||
private ListView listView;
|
||||
private PlateView plateView;
|
||||
private List<TieredRemnant> remnants = new();
|
||||
private int selectedIndex = -1;
|
||||
|
||||
public RemnantViewerForm()
|
||||
{
|
||||
Text = "Remnants";
|
||||
Size = new System.Drawing.Size(360, 400);
|
||||
StartPosition = FormStartPosition.Manual;
|
||||
FormBorderStyle = FormBorderStyle.SizableToolWindow;
|
||||
ShowInTaskbar = false;
|
||||
TopMost = true;
|
||||
|
||||
listView = new ListView
|
||||
{
|
||||
Dock = DockStyle.Fill,
|
||||
View = View.Details,
|
||||
FullRowSelect = true,
|
||||
GridLines = true,
|
||||
MultiSelect = false,
|
||||
HideSelection = false,
|
||||
};
|
||||
|
||||
listView.Columns.Add("P", 28, HorizontalAlignment.Center);
|
||||
listView.Columns.Add("Size", 110, HorizontalAlignment.Left);
|
||||
listView.Columns.Add("Area", 65, HorizontalAlignment.Right);
|
||||
listView.Columns.Add("Location", 110, HorizontalAlignment.Left);
|
||||
|
||||
listView.SelectedIndexChanged += ListView_SelectedIndexChanged;
|
||||
|
||||
Controls.Add(listView);
|
||||
}
|
||||
|
||||
protected override bool ProcessDialogKey(Keys keyData)
|
||||
{
|
||||
if (keyData == Keys.Escape)
|
||||
{
|
||||
Close();
|
||||
return true;
|
||||
}
|
||||
return base.ProcessDialogKey(keyData);
|
||||
}
|
||||
|
||||
public void LoadRemnants(List<TieredRemnant> tieredRemnants, PlateView view)
|
||||
{
|
||||
plateView = view;
|
||||
remnants = tieredRemnants;
|
||||
selectedIndex = -1;
|
||||
|
||||
listView.BeginUpdate();
|
||||
listView.Items.Clear();
|
||||
|
||||
foreach (var tr in remnants)
|
||||
{
|
||||
var item = new ListViewItem(tr.Priority.ToString());
|
||||
item.SubItems.Add($"{tr.Box.Width:F2} x {tr.Box.Length:F2}");
|
||||
item.SubItems.Add($"{tr.Box.Area():F1}");
|
||||
item.SubItems.Add($"({tr.Box.X:F2}, {tr.Box.Y:F2})");
|
||||
|
||||
switch (tr.Priority)
|
||||
{
|
||||
case 0: item.BackColor = Color.FromArgb(220, 255, 220); break;
|
||||
case 1: item.BackColor = Color.FromArgb(255, 255, 210); break;
|
||||
default: item.BackColor = Color.FromArgb(255, 220, 220); break;
|
||||
}
|
||||
|
||||
listView.Items.Add(item);
|
||||
}
|
||||
|
||||
listView.EndUpdate();
|
||||
}
|
||||
|
||||
private void ListView_SelectedIndexChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (plateView == null)
|
||||
return;
|
||||
|
||||
if (listView.SelectedIndices.Count == 0)
|
||||
{
|
||||
selectedIndex = -1;
|
||||
plateView.DebugRemnants = null;
|
||||
plateView.DebugRemnantPriorities = null;
|
||||
return;
|
||||
}
|
||||
|
||||
selectedIndex = listView.SelectedIndices[0];
|
||||
|
||||
if (selectedIndex >= 0 && selectedIndex < remnants.Count)
|
||||
{
|
||||
var tr = remnants[selectedIndex];
|
||||
plateView.DebugRemnants = new List<Box> { tr.Box };
|
||||
plateView.DebugRemnantPriorities = new List<int> { tr.Priority };
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnFormClosing(FormClosingEventArgs e)
|
||||
{
|
||||
if (plateView != null)
|
||||
{
|
||||
plateView.DebugRemnants = null;
|
||||
plateView.DebugRemnantPriorities = null;
|
||||
}
|
||||
|
||||
base.OnFormClosing(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user