From 1d46ce2298ffcc0e32ca2ef6d0105fbfa82b0640 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Sat, 7 Mar 2026 20:06:28 -0500 Subject: [PATCH] feat: add BestFitViewerForm for visualizing best-fit nesting results Co-Authored-By: Claude Opus 4.6 --- OpenNest/Forms/BestFitViewerForm.Designer.cs | 51 ++++++ OpenNest/Forms/BestFitViewerForm.cs | 183 +++++++++++++++++++ 2 files changed, 234 insertions(+) create mode 100644 OpenNest/Forms/BestFitViewerForm.Designer.cs create mode 100644 OpenNest/Forms/BestFitViewerForm.cs diff --git a/OpenNest/Forms/BestFitViewerForm.Designer.cs b/OpenNest/Forms/BestFitViewerForm.Designer.cs new file mode 100644 index 0000000..a53a48a --- /dev/null +++ b/OpenNest/Forms/BestFitViewerForm.Designer.cs @@ -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; + } +} diff --git a/OpenNest/Forms/BestFitViewerForm.cs b/OpenNest/Forms/BestFitViewerForm.cs new file mode 100644 index 0000000..90f70ff --- /dev/null +++ b/OpenNest/Forms/BestFitViewerForm.cs @@ -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; + } + } + } +}