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:
2026-03-16 22:49:51 -04:00
parent 896cb536dd
commit 9d40d78562
4 changed files with 234 additions and 3 deletions

View File

@@ -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);

View File

@@ -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;
}
}

View File

@@ -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))

View 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);
}
}
}