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:
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -370,7 +370,7 @@ namespace OpenNest.Controls
|
|||||||
Invalidate();
|
Invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawPlate(Graphics g)
|
protected void DrawPlate(Graphics g)
|
||||||
{
|
{
|
||||||
var plateRect = new RectangleF
|
var plateRect = new RectangleF
|
||||||
{
|
{
|
||||||
@@ -441,7 +441,7 @@ namespace OpenNest.Controls
|
|||||||
plateRect.Height);
|
plateRect.Height);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawParts(Graphics g)
|
protected void DrawParts(Graphics g)
|
||||||
{
|
{
|
||||||
var viewBounds = new RectangleF(-origin.X, -origin.Y, Width, Height);
|
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 stationaryBoxes = new List<Box>(stationaryParts.Count);
|
||||||
|
|
||||||
var opposite = Helper.OppositeDirection(direction);
|
var opposite = Helper.OppositeDirection(direction);
|
||||||
|
var halfSpacing = Plate.PartSpacing / 2;
|
||||||
|
|
||||||
foreach (var part in stationaryParts)
|
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);
|
stationaryBoxes.Add(part.BoundingBox);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -868,9 +871,9 @@ namespace OpenNest.Controls
|
|||||||
|
|
||||||
foreach (var selected in SelectedParts)
|
foreach (var selected in SelectedParts)
|
||||||
{
|
{
|
||||||
// Get offset lines for the moving part.
|
// Get offset lines for the moving part (half-spacing, symmetric with stationary).
|
||||||
var movingLines = Plate.PartSpacing > 0
|
var movingLines = halfSpacing > 0
|
||||||
? Helper.GetOffsetPartLines(selected.BasePart, Plate.PartSpacing, direction)
|
? Helper.GetOffsetPartLines(selected.BasePart, halfSpacing, direction)
|
||||||
: Helper.GetPartLines(selected.BasePart, direction);
|
: Helper.GetPartLines(selected.BasePart, direction);
|
||||||
|
|
||||||
var movingBox = selected.BoundingBox;
|
var movingBox = selected.BoundingBox;
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
using OpenNest.Controls;
|
using OpenNest.Controls;
|
||||||
using OpenNest.Engine.BestFit;
|
using OpenNest.Engine.BestFit;
|
||||||
using OpenNest.Geometry;
|
|
||||||
using OpenNest.Math;
|
|
||||||
|
|
||||||
namespace OpenNest.Forms
|
namespace OpenNest.Forms
|
||||||
{
|
{
|
||||||
@@ -85,8 +82,8 @@ namespace OpenNest.Forms
|
|||||||
for (var i = 0; i < count; i++)
|
for (var i = 0; i < count; i++)
|
||||||
{
|
{
|
||||||
var result = results[i];
|
var result = results[i];
|
||||||
var view = CreateCellView(result, drawing, i + 1);
|
var cell = CreateCell(result, drawing, i + 1);
|
||||||
gridPanel.Controls.Add(view, i % Columns, i / Columns);
|
gridPanel.Controls.Add(cell, i % Columns, i / Columns);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -99,7 +96,7 @@ namespace OpenNest.Forms
|
|||||||
total, kept, findMs / 1000.0, sw.Elapsed.TotalSeconds, count);
|
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;
|
var bgColor = result.Keep ? KeptColor : DroppedColor;
|
||||||
|
|
||||||
@@ -114,70 +111,27 @@ namespace OpenNest.Forms
|
|||||||
EdgeSpacingColor = bgColor
|
EdgeSpacingColor = bgColor
|
||||||
};
|
};
|
||||||
|
|
||||||
var view = new PlateView(colorScheme);
|
var cell = new BestFitCell(colorScheme);
|
||||||
view.DrawOrigin = false;
|
cell.Dock = DockStyle.Fill;
|
||||||
view.DrawBounds = false;
|
cell.Plate.Size = new Geometry.Size(
|
||||||
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.BoundingWidth,
|
||||||
result.BoundingHeight);
|
result.BoundingHeight);
|
||||||
|
|
||||||
var parts = result.BuildParts(drawing);
|
var parts = result.BuildParts(drawing);
|
||||||
|
|
||||||
foreach (var part in parts)
|
foreach (var part in parts)
|
||||||
view.Plate.Parts.Add(part);
|
cell.Plate.Parts.Add(part);
|
||||||
|
|
||||||
view.Paint += (sender, e) =>
|
cell.SetMetadata(result, rank);
|
||||||
{
|
|
||||||
PaintMetadata(e.Graphics, view, result, rank);
|
|
||||||
};
|
|
||||||
|
|
||||||
view.Resize += (sender, e) =>
|
cell.DoubleClick += (sender, e) =>
|
||||||
{
|
|
||||||
view.ZoomToFit(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
view.DoubleClick += (sender, e) =>
|
|
||||||
{
|
{
|
||||||
SelectedResult = result;
|
SelectedResult = result;
|
||||||
DialogResult = DialogResult.OK;
|
DialogResult = DialogResult.OK;
|
||||||
Close();
|
Close();
|
||||||
};
|
};
|
||||||
|
|
||||||
view.Cursor = Cursors.Hand;
|
return cell;
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user