feat: add BestFitViewerForm for visualizing best-fit nesting results

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-07 20:06:28 -05:00
parent c575d82f80
commit 1d46ce2298
2 changed files with 234 additions and 0 deletions

View File

@@ -0,0 +1,51 @@
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;
}
}

View File

@@ -0,0 +1,183 @@
using System.Collections.Generic;
using System.Diagnostics;
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 const int Columns = 5;
private const int RowHeight = 300;
private const int MaxResults = 50;
private const double ViewerStepSize = 1.0;
private static readonly Color KeptColor = Color.FromArgb(0, 0, 100);
private static readonly Color DroppedColor = Color.FromArgb(100, 0, 0);
private readonly Drawing drawing;
private readonly Plate plate;
public BestFitResult SelectedResult { get; private set; }
public BestFitViewerForm(Drawing drawing, Plate plate)
{
this.drawing = drawing;
this.plate = plate;
InitializeComponent();
Shown += BestFitViewerForm_Shown;
}
private void BestFitViewerForm_Shown(object sender, System.EventArgs e)
{
Cursor = Cursors.WaitCursor;
try
{
PopulateGrid(drawing, plate);
}
finally
{
Cursor = Cursors.Default;
}
}
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 sw = Stopwatch.StartNew();
var finder = new BestFitFinder(plate.Size.Width, plate.Size.Height);
var results = finder.FindBestFits(drawing, plate.PartSpacing, ViewerStepSize);
var findMs = sw.ElapsedMilliseconds;
var total = results.Count;
var kept = 0;
foreach (var r in results)
{
if (r.Keep) kept++;
}
var count = System.Math.Min(total, MaxResults);
var rows = (int)System.Math.Ceiling(count / (double)Columns);
gridPanel.RowCount = rows;
gridPanel.RowStyles.Clear();
for (var i = 0; i < rows; i++)
gridPanel.RowStyles.Add(new RowStyle(SizeType.Absolute, RowHeight));
gridPanel.SuspendLayout();
try
{
for (var i = 0; i < count; i++)
{
var result = results[i];
var view = CreateCellView(result, drawing, i + 1);
gridPanel.Controls.Add(view, i % Columns, i / Columns);
}
}
finally
{
gridPanel.ResumeLayout(true);
}
sw.Stop();
Text = string.Format("Best-Fit Viewer — {0} candidates ({1} kept) | Compute: {2:F1}s | Total: {3:F1}s | Showing {4}",
total, kept, findMs / 1000.0, sw.Elapsed.TotalSeconds, count);
}
private PlateView CreateCellView(BestFitResult result, Drawing drawing, int rank)
{
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, rank);
};
view.Resize += (sender, e) =>
{
view.ZoomToFit(false);
};
view.DoubleClick += (sender, e) =>
{
SelectedResult = result;
DialogResult = DialogResult.OK;
Close();
};
view.Cursor = Cursors.Hand;
return view;
}
private void PaintMetadata(Graphics g, PlateView view, BestFitResult result, int rank)
{
var font = view.Font;
var brush = Brushes.White;
var lineHeight = font.GetHeight(g) + 1;
var lines = new[]
{
string.Format("#{0} {1:F1}x{2:F1} Area={3:F1}",
rank, result.BoundingWidth, result.BoundingHeight, result.RotatedArea),
string.Format("Util={0:P1} Rot={1:F1}\u00b0",
result.Utilization,
Angle.ToDegrees(result.OptimalRotation)),
result.Keep ? "" : result.Reason
};
var y = 2f;
foreach (var line in lines)
{
if (line.Length == 0)
continue;
g.DrawString(line, font, brush, 2, y);
y += lineHeight;
}
}
}
}