feat: add BestFitCell control with screen-space text overlay

Extract a BestFitCell subclass from PlateView for the Best-Fit Viewer
grid cells. Text metadata is now painted in screen coordinates (after
resetting the graphics transform) so it stays fixed regardless of zoom.
Parts auto-zoom to fit on every resize.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 13:26:29 -04:00
parent e078ef4b77
commit 2225c4ef09
3 changed files with 101 additions and 62 deletions

View File

@@ -0,0 +1,82 @@
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;
using OpenNest.Engine.BestFit;
using OpenNest.Math;
namespace OpenNest.Controls
{
public class BestFitCell : PlateView
{
private string[] metadataLines;
public BestFitResult Result { get; set; }
public BestFitCell(ColorScheme colorScheme)
: base(colorScheme)
{
DrawOrigin = false;
DrawBounds = false;
AllowPan = false;
AllowSelect = false;
AllowZoom = false;
AllowDrop = false;
Cursor = Cursors.Hand;
}
public void SetMetadata(BestFitResult result, int rank)
{
Result = result;
metadataLines = 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
};
}
protected override void OnResize(System.EventArgs e)
{
base.OnResize(e);
if (Plate.Parts.Count > 0)
ZoomToFit(false);
}
protected override void OnPaint(PaintEventArgs e)
{
e.Graphics.SmoothingMode = SmoothingMode.HighSpeed;
e.Graphics.TranslateTransform(origin.X, origin.Y);
DrawPlate(e.Graphics);
DrawParts(e.Graphics);
e.Graphics.ResetTransform();
PaintMetadata(e.Graphics);
}
private void PaintMetadata(Graphics g)
{
if (metadataLines == null)
return;
var font = Font;
var brush = Brushes.White;
var lineHeight = font.GetHeight(g) + 1;
var y = 2f;
foreach (var line in metadataLines)
{
if (line.Length == 0)
continue;
g.DrawString(line, font, brush, 2, y);
y += lineHeight;
}
}
}
}

View File

@@ -370,7 +370,7 @@ namespace OpenNest.Controls
Invalidate();
}
private void DrawPlate(Graphics g)
protected void DrawPlate(Graphics g)
{
var plateRect = new RectangleF
{
@@ -441,7 +441,7 @@ namespace OpenNest.Controls
plateRect.Height);
}
private void DrawParts(Graphics g)
protected void DrawParts(Graphics g)
{
var viewBounds = new RectangleF(-origin.X, -origin.Y, Width, Height);
@@ -856,10 +856,13 @@ namespace OpenNest.Controls
var stationaryBoxes = new List<Box>(stationaryParts.Count);
var opposite = Helper.OppositeDirection(direction);
var halfSpacing = Plate.PartSpacing / 2;
foreach (var part in stationaryParts)
{
stationaryLines.Add(Helper.GetPartLines(part.BasePart, opposite));
stationaryLines.Add(halfSpacing > 0
? Helper.GetOffsetPartLines(part.BasePart, halfSpacing, opposite)
: Helper.GetPartLines(part.BasePart, opposite));
stationaryBoxes.Add(part.BoundingBox);
}
@@ -868,9 +871,9 @@ namespace OpenNest.Controls
foreach (var selected in SelectedParts)
{
// Get offset lines for the moving part.
var movingLines = Plate.PartSpacing > 0
? Helper.GetOffsetPartLines(selected.BasePart, Plate.PartSpacing, direction)
// Get offset lines for the moving part (half-spacing, symmetric with stationary).
var movingLines = halfSpacing > 0
? Helper.GetOffsetPartLines(selected.BasePart, halfSpacing, direction)
: Helper.GetPartLines(selected.BasePart, direction);
var movingBox = selected.BoundingBox;

View File

@@ -1,11 +1,8 @@
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
{
@@ -85,8 +82,8 @@ namespace OpenNest.Forms
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);
var cell = CreateCell(result, drawing, i + 1);
gridPanel.Controls.Add(cell, i % Columns, i / Columns);
}
}
finally
@@ -99,7 +96,7 @@ namespace OpenNest.Forms
total, kept, findMs / 1000.0, sw.Elapsed.TotalSeconds, count);
}
private PlateView CreateCellView(BestFitResult result, Drawing drawing, int rank)
private BestFitCell CreateCell(BestFitResult result, Drawing drawing, int rank)
{
var bgColor = result.Keep ? KeptColor : DroppedColor;
@@ -114,70 +111,27 @@ namespace OpenNest.Forms
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(
var cell = new BestFitCell(colorScheme);
cell.Dock = DockStyle.Fill;
cell.Plate.Size = new Geometry.Size(
result.BoundingWidth,
result.BoundingHeight);
var parts = result.BuildParts(drawing);
foreach (var part in parts)
view.Plate.Parts.Add(part);
cell.Plate.Parts.Add(part);
view.Paint += (sender, e) =>
{
PaintMetadata(e.Graphics, view, result, rank);
};
cell.SetMetadata(result, rank);
view.Resize += (sender, e) =>
{
view.ZoomToFit(false);
};
view.DoubleClick += (sender, e) =>
cell.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;
}
return cell;
}
}
}