12 KiB
Best-Fit Viewer Implementation Plan
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Add a BestFitViewerForm that shows all pair candidates in a dense 5-column grid with metadata overlay, similar to PEP's best-fit viewer.
Architecture: A modal Form with a scrollable TableLayoutPanel (5 columns). Each cell is a read-only PlateView with the pair's two parts placed on it. Metadata is painted as overlay text on each cell. Dropped candidates use a different background color. Invoked from Tools menu when a drawing and plate are available.
Tech Stack: WinForms, BestFitFinder from OpenNest.Engine.BestFit, PlateView control.
Task 1: Extract BuildPairParts to a static helper
NestEngine.BuildPairParts is private and contains the pair-building logic we need. Extract it to a public static method so both NestEngine and the new form can use it.
Files:
- Modify:
OpenNest.Engine/NestEngine.cs
Step 1: Make BuildPairParts internal static
In OpenNest.Engine/NestEngine.cs, change the method signature from private instance to internal static. It doesn't use any instance state — only BestFitResult and Drawing parameters.
Change:
private List<Part> BuildPairParts(BestFitResult bestFit, Drawing drawing)
To:
internal static List<Part> BuildPairParts(BestFitResult bestFit, Drawing drawing)
Step 2: Build and verify
Run: dotnet build OpenNest.sln
Expected: Build succeeds with no errors.
Step 3: Commit
git add OpenNest.Engine/NestEngine.cs
git commit -m "refactor: make BuildPairParts internal static for reuse"
Task 2: Create BestFitViewerForm
Files:
- Create:
OpenNest/Forms/BestFitViewerForm.cs - Create:
OpenNest/Forms/BestFitViewerForm.Designer.cs
Step 1: Create the Designer file
Create OpenNest/Forms/BestFitViewerForm.Designer.cs with a TableLayoutPanel (5 columns, auto-scroll, dock-fill) inside the form. Form should be sizable, start centered on parent, ~1200x800 default size, title "Best-Fit Viewer".
namespace OpenNest.Forms
{
partial class BestFitViewerForm
{
private System.ComponentModel.IContainer components = null;
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
components.Dispose();
base.Dispose(disposing);
}
private void InitializeComponent()
{
this.gridPanel = new System.Windows.Forms.TableLayoutPanel();
this.SuspendLayout();
//
// gridPanel
//
this.gridPanel.AutoScroll = true;
this.gridPanel.ColumnCount = 5;
this.gridPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 20F));
this.gridPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 20F));
this.gridPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 20F));
this.gridPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 20F));
this.gridPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 20F));
this.gridPanel.Dock = System.Windows.Forms.DockStyle.Fill;
this.gridPanel.Location = new System.Drawing.Point(0, 0);
this.gridPanel.Name = "gridPanel";
this.gridPanel.RowCount = 1;
this.gridPanel.RowStyles.Add(new System.Windows.Forms.RowStyle());
this.gridPanel.Size = new System.Drawing.Size(1200, 800);
this.gridPanel.TabIndex = 0;
//
// BestFitViewerForm
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(1200, 800);
this.Controls.Add(this.gridPanel);
this.KeyPreview = true;
this.Name = "BestFitViewerForm";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "Best-Fit Viewer";
this.ResumeLayout(false);
}
private System.Windows.Forms.TableLayoutPanel gridPanel;
}
}
Step 2: Create the code-behind file
Create OpenNest/Forms/BestFitViewerForm.cs. The constructor takes a Drawing and a Plate. It calls BestFitFinder.FindBestFits() to get all candidates, then for each result:
- Creates a
PlateViewconfigured read-only (no pan/zoom/select/origin, no plate outline) - Sizes the PlateView's plate to the pair bounding box
- Builds pair parts via
NestEngine.BuildPairParts()and adds them to the plate - Sets background color based on
Keep(kept = default, dropped = maroon) - Subscribes to
Paintto overlay metadata text
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;
using OpenNest.Controls;
using OpenNest.Engine.BestFit;
using OpenNest.Geometry;
using OpenNest.Math;
namespace OpenNest.Forms
{
public partial class BestFitViewerForm : Form
{
private static readonly Color KeptColor = Color.FromArgb(0, 0, 100);
private static readonly Color DroppedColor = Color.FromArgb(100, 0, 0);
public BestFitViewerForm(Drawing drawing, Plate plate)
{
InitializeComponent();
PopulateGrid(drawing, plate);
}
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
{
if (keyData == Keys.Escape)
{
Close();
return true;
}
return base.ProcessCmdKey(ref msg, keyData);
}
private void PopulateGrid(Drawing drawing, Plate plate)
{
var finder = new BestFitFinder(plate.Size.Width, plate.Size.Height);
var results = finder.FindBestFits(drawing, plate.PartSpacing);
var rows = (int)System.Math.Ceiling(results.Count / 5.0);
gridPanel.RowCount = rows;
gridPanel.RowStyles.Clear();
for (var i = 0; i < rows; i++)
gridPanel.RowStyles.Add(new RowStyle(SizeType.Absolute, 200));
for (var i = 0; i < results.Count; i++)
{
var result = results[i];
var view = CreateCellView(result, drawing);
gridPanel.Controls.Add(view, i % 5, i / 5);
}
}
private PlateView CreateCellView(BestFitResult result, Drawing drawing)
{
var bgColor = result.Keep ? KeptColor : DroppedColor;
var colorScheme = new ColorScheme
{
BackgroundColor = bgColor,
LayoutOutlineColor = bgColor,
LayoutFillColor = bgColor,
BoundingBoxColor = bgColor,
RapidColor = Color.DodgerBlue,
OriginColor = bgColor,
EdgeSpacingColor = bgColor
};
var view = new PlateView(colorScheme);
view.DrawOrigin = false;
view.DrawBounds = false;
view.AllowPan = false;
view.AllowSelect = false;
view.AllowZoom = false;
view.AllowDrop = false;
view.Dock = DockStyle.Fill;
view.Plate.Size = new Geometry.Size(
result.BoundingWidth,
result.BoundingHeight);
var parts = NestEngine.BuildPairParts(result, drawing);
foreach (var part in parts)
view.Plate.Parts.Add(part);
view.Paint += (sender, e) =>
{
PaintMetadata(e.Graphics, view, result);
};
view.HandleCreated += (sender, e) =>
{
view.ZoomToFit(true);
};
return view;
}
private void PaintMetadata(Graphics g, PlateView view, BestFitResult result)
{
var font = view.Font;
var brush = Brushes.White;
var y = 2f;
var lineHeight = font.GetHeight(g) + 1;
var lines = new[]
{
string.Format("RotatedArea={0:F4}", result.RotatedArea),
string.Format("{0:F4}x{1:F4}={2:F4}",
result.BoundingWidth, result.BoundingHeight, result.RotatedArea),
string.Format("Why={0}", result.Keep ? "0" : result.Reason),
string.Format("Type={0} Test={1} Spacing={2}",
result.Candidate.StrategyType,
result.Candidate.TestNumber,
result.Candidate.Spacing),
string.Format("Util={0:P0} Rot={1:F1}°",
result.Utilization,
Angle.ToDegrees(result.OptimalRotation))
};
foreach (var line in lines)
{
g.DrawString(line, font, brush, 2, y);
y += lineHeight;
}
}
}
}
Step 3: Build and verify
Run: dotnet build OpenNest.sln
Expected: Build succeeds.
Step 4: Commit
git add OpenNest/Forms/BestFitViewerForm.cs OpenNest/Forms/BestFitViewerForm.Designer.cs
git commit -m "feat: add BestFitViewerForm with pair candidate grid"
Task 3: Add menu item to MainForm
Files:
- Modify:
OpenNest/Forms/MainForm.Designer.cs - Modify:
OpenNest/Forms/MainForm.cs
Step 1: Add the menu item field and wire it up in Designer
In MainForm.Designer.cs:
- Add field declaration near the other
mnuTools*fields (~line 1198):
private System.Windows.Forms.ToolStripMenuItem mnuToolsBestFitViewer;
- Add instantiation in
InitializeComponent()near other mnuTools instantiations (~line 62):
this.mnuToolsBestFitViewer = new System.Windows.Forms.ToolStripMenuItem();
- Add to the Tools menu
DropDownItemsarray (aftermnuToolsMeasureArea, ~line 413-420). InsertmnuToolsBestFitViewerbefore thetoolStripMenuItem14separator:
this.mnuTools.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.mnuToolsMeasureArea,
this.mnuToolsBestFitViewer,
this.mnuToolsAlign,
this.toolStripMenuItem14,
this.mnuSetOffsetIncrement,
this.mnuSetRotationIncrement,
this.toolStripMenuItem15,
this.mnuToolsOptions});
- Add menu item configuration after the
mnuToolsMeasureAreablock (~line 431):
//
// mnuToolsBestFitViewer
//
this.mnuToolsBestFitViewer.Name = "mnuToolsBestFitViewer";
this.mnuToolsBestFitViewer.Size = new System.Drawing.Size(214, 22);
this.mnuToolsBestFitViewer.Text = "Best-Fit Viewer";
this.mnuToolsBestFitViewer.Click += new System.EventHandler(this.BestFitViewer_Click);
Step 2: Add the click handler in MainForm.cs
Add a method to MainForm.cs that opens the form. It needs the active EditNestForm to get the current plate and a selected drawing. If no drawing is available from the selected plate's parts, show a message.
private void BestFitViewer_Click(object sender, EventArgs e)
{
if (activeForm == null)
return;
var plate = activeForm.PlateView.Plate;
var drawing = activeForm.Nest.Drawings.Count > 0
? activeForm.Nest.Drawings[0]
: null;
if (drawing == null)
{
MessageBox.Show("No drawings available.", "Best-Fit Viewer",
MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
using (var form = new BestFitViewerForm(drawing, plate))
{
form.ShowDialog(this);
}
}
Step 3: Build and verify
Run: dotnet build OpenNest.sln
Expected: Build succeeds.
Step 4: Commit
git add OpenNest/Forms/MainForm.Designer.cs OpenNest/Forms/MainForm.cs
git commit -m "feat: add Best-Fit Viewer menu item under Tools"
Task 4: Manual smoke test
Step 1: Run the application
Run: dotnet run --project OpenNest
Step 2: Test the flow
- Open or create a nest file
- Import a DXF drawing
- Go to Tools > Best-Fit Viewer
- Verify the grid appears with pair candidates
- Verify kept candidates have dark blue background
- Verify dropped candidates have dark red/maroon background
- Verify metadata text is readable on each cell
- Verify ESC closes the dialog
- Verify scroll works when many results exist
Step 3: Fix any visual issues
Adjust cell heights, font sizes, or zoom-to-fit timing if needed.
Step 4: Final commit
git add -A
git commit -m "fix: polish BestFitViewerForm layout and appearance"