diff --git a/OpenNest.WinForms.Tests/Controls/DrawingSetPreviewTests.cs b/OpenNest.WinForms.Tests/Controls/DrawingSetPreviewTests.cs
index 83c35fde..71352f7b 100644
--- a/OpenNest.WinForms.Tests/Controls/DrawingSetPreviewTests.cs
+++ b/OpenNest.WinForms.Tests/Controls/DrawingSetPreviewTests.cs
@@ -64,6 +64,25 @@ public class DrawingSetPreviewTests
}
}
+ [Fact]
+ public void Arrange_FillsANarrowPaneWhenLabelCellsSetTheRowWidth()
+ {
+ // A tall, narrow pane where the minimum label cell, not the parts, limits each row.
+ var sizes = new List<(double, double)>
+ {
+ (45, 75), (40, 75), (40, 72), (40, 72), (22, 40), (35, 33), (35, 33), (60, 25),
+ (60, 25), (75, 22), (75, 22), (22, 22), (35, 30), (35, 30), (20, 20), (25, 15),
+ (15, 15), (40, 15), (20, 15), (6, 10), (6, 10),
+ };
+
+ var result = DrawingSetPreviewLayout.Arrange(sizes, 400, 590, 17, 90);
+
+ // Rows used to be fixed at four label cells, leaving the pane 40% empty at about half this scale.
+ Assert.True(result.Scale > 2.2, $"scale {result.Scale}");
+ Assert.True(result.Width * result.Scale <= 400 + 1e-6);
+ Assert.InRange(result.Height * result.Scale, 585, 590 + 1e-6);
+ }
+
[Fact]
public void Arrange_UsesTheViewShape()
{
diff --git a/OpenNest/Controls/DrawingSetPreviewLayout.cs b/OpenNest/Controls/DrawingSetPreviewLayout.cs
index b7b70ee3..1f2be9ef 100644
--- a/OpenNest/Controls/DrawingSetPreviewLayout.cs
+++ b/OpenNest/Controls/DrawingSetPreviewLayout.cs
@@ -6,15 +6,19 @@ namespace OpenNest.Controls
{
///
/// Packs a set's drawings into rows for , every one at
- /// the same scale and in its own orientation. Rows are filled tallest first (next-fit
- /// decreasing height) and each row's parts share a baseline with a label strip beneath it.
- /// The row width is chosen to give the largest fit scale in the view, and the label strip
- /// and minimum cell width are fixed in pixels, so they are solved together with that scale.
+ /// the same scale and in its own orientation. Rows are filled tallest first and each row's
+ /// parts share a baseline with a label strip beneath it. The label strip and minimum cell
+ /// width are fixed in pixels, so the layout is solved for the largest scale that fits the view.
///
+ ///
+ /// At a given scale, filling each row as far as the view's width allows beats any other split
+ /// of the tallest-first order into rows: each row then starts no earlier, so it is no taller,
+ /// and there are no more rows. A larger scale only widens cells, so the packed height grows
+ /// with scale and the largest scale that fits is found by bisection.
+ ///
internal static class DrawingSetPreviewLayout
{
- private const int RowWidthCandidates = 48;
- private const int ScaleIterations = 4;
+ private const int ScaleIterations = 60;
/// Where one drawing lands: its bounding box's lower-left corner and its label cell, in world units (Y up).
internal readonly record struct Placement(int Index, double PartX, double PartY, double CellX, double CellWidth, double LabelTop);
@@ -50,6 +54,9 @@ namespace OpenNest.Controls
viewWidth = System.Math.Max(viewWidth, 1);
viewHeight = System.Math.Max(viewHeight, 1);
+ labelPixels = System.Math.Max(labelPixels, 0);
+ // A cell wider than the view could never be placed, so the floor gives way to the view.
+ minCellPixels = System.Math.Clamp(minCellPixels, 0, viewWidth);
var clean = sizes.Select(s => (Width: Positive(s.Width), Height: Positive(s.Height))).ToArray();
var largest = clean.Max(s => System.Math.Max(s.Width, s.Height));
var spacing = largest * 0.04;
@@ -58,40 +65,46 @@ namespace OpenNest.Controls
.ThenBy(i => i)
.ToArray();
- // Any row narrower than the widest part (plus its spacing) cannot hold it; wider
- // than every part side by side is one row. Candidates are spread geometrically between.
- var narrowest = clean.Max(s => s.Width);
- var widest = clean.Sum(s => s.Width) + spacing * clean.Length;
- Result best = null;
- for (var c = 0; c <= RowWidthCandidates; c++)
- {
- var rowWidth = narrowest * System.Math.Pow(widest / narrowest, (double)c / RowWidthCandidates);
- var scale = System.Math.Min(viewWidth / widest, viewHeight / clean.Sum(s => s.Height));
- Result result = null;
- for (var i = 0; i < ScaleIterations; i++)
- {
- result = Pack(clean, order, rowWidth, spacing, labelPixels / scale, minCellPixels / scale);
- scale = System.Math.Min(viewWidth / result.Width, viewHeight / result.Height);
- }
+ Result PackAt(double scale) =>
+ Pack(clean, order, viewWidth / scale, spacing, labelPixels / scale, minCellPixels / scale);
- // The label strip was sized for the previous scale; repack once at the final one
- // so the reported scale and the strip agree.
- result = Pack(clean, order, rowWidth, spacing, labelPixels / scale, minCellPixels / scale);
- result = new Result
- {
- Placements = result.Placements,
- Width = result.Width,
- Height = result.Height,
- LabelHeight = result.LabelHeight,
- Scale = System.Math.Min(viewWidth / result.Width, viewHeight / result.Height),
- };
- if (best == null || result.Scale > best.Scale)
- best = result;
+ bool Fits(Result packed, double scale) =>
+ packed.Width * scale <= viewWidth * (1 + 1e-9) && packed.Height * scale <= viewHeight * (1 + 1e-9);
+
+ // No scale can exceed the one that fits the widest or the tallest part alone.
+ var high = System.Math.Min(viewWidth / clean.Max(s => s.Width), viewHeight / clean.Max(s => s.Height));
+ var low = high * 1e-9;
+ var packed = PackAt(low);
+ if (!Fits(packed, low))
+ {
+ // Even shrunk to nothing, the label strips of the rows overflow the view: keep
+ // that packing and shrink it to fit, labels included.
+ return WithScale(packed, System.Math.Min(viewWidth / packed.Width, viewHeight / packed.Height));
}
- return best;
+ if (Fits(PackAt(high), high))
+ low = high;
+ for (var i = 0; i < ScaleIterations && high - low > low * 1e-9; i++)
+ {
+ var scale = (low + high) / 2;
+ if (Fits(PackAt(scale), scale))
+ low = scale;
+ else
+ high = scale;
+ }
+
+ return WithScale(PackAt(low), low);
}
+ private static Result WithScale(Result packed, double scale) => new()
+ {
+ Placements = packed.Placements,
+ Width = packed.Width,
+ Height = packed.Height,
+ LabelHeight = packed.LabelHeight,
+ Scale = scale,
+ };
+
private static Result Pack((double Width, double Height)[] sizes, int[] order, double rowWidth, double spacing,
double labelHeight, double minCell)
{
@@ -101,7 +114,7 @@ namespace OpenNest.Controls
var start = 0;
while (start < order.Length)
{
- // Next-fit: take parts while they fit the row; a row always takes at least one.
+ // Take parts while they fit the row; a row always takes at least one.
var x = 0.0;
var end = start;
while (end < order.Length)