using OpenNest.Controls;
using OpenNest.Data;
using OpenNest.Geometry;
using OpenNest.Tests.Data;
namespace OpenNest.WinForms.Tests.Controls;
///
/// The set preview packs every drawing into one view at one scale, without overlaps, each in
/// its own orientation, and stays inside the view.
///
public class DrawingSetPreviewTests
{
private static Drawing Rectangle(string name, double width, double height, double x = 0, double y = 0)
{
var program = new CNC.Program();
program.Codes.Add(new CNC.RapidMove(new Vector(x, y)));
program.Codes.Add(new CNC.LinearMove(new Vector(x + width, y)));
program.Codes.Add(new CNC.LinearMove(new Vector(x + width, y + height)));
program.Codes.Add(new CNC.LinearMove(new Vector(x, y + height)));
program.Codes.Add(new CNC.LinearMove(new Vector(x, y)));
return new Drawing(name, program);
}
[Fact]
public void Arrange_PlacesEveryCellApartAndInsideTheLayout()
{
var sizes = new List<(double, double)> { (40, 10), (3, 3), (12, 30), (1, 0.5), (25, 25), (60, 2), (5, 8) };
var result = DrawingSetPreviewLayout.Arrange(sizes, 800, 600, 18, 90);
Assert.Equal(sizes.Count, result.Placements.Count);
Assert.True(result.Scale > 0);
Assert.True(result.Width * result.Scale <= 800 + 1e-6);
Assert.True(result.Height * result.Scale <= 600 + 1e-6);
// The strip is solved together with the scale, so it lands within a pixel of the request.
Assert.InRange(result.LabelHeight * result.Scale, 17, 19);
// Each cell covers its part's width (at least the minimum) and its label strip below.
var cells = result.Placements.Select(p =>
{
var (width, height) = sizes[p.Index];
Assert.InRange(p.PartX, p.CellX - 1e-9, p.CellX + p.CellWidth - width + 1e-9);
Assert.True(p.CellWidth * result.Scale >= 89);
Assert.Equal(p.LabelTop, p.PartY, 9);
return (Left: p.CellX, Right: p.CellX + p.CellWidth, Bottom: p.PartY - result.LabelHeight, Top: p.PartY + height);
}).ToList();
foreach (var cell in cells)
{
Assert.InRange(cell.Left, -1e-9, result.Width + 1e-9);
Assert.InRange(cell.Right, -1e-9, result.Width + 1e-9);
Assert.InRange(cell.Bottom, -result.Height - 1e-9, 1e-9);
Assert.InRange(cell.Top, -result.Height - 1e-9, 1e-9);
}
for (var i = 0; i < cells.Count; i++)
for (var j = i + 1; j < cells.Count; j++)
{
var a = cells[i];
var b = cells[j];
var apart = a.Right <= b.Left + 1e-9 || b.Right <= a.Left + 1e-9 || a.Top <= b.Bottom + 1e-9 || b.Top <= a.Bottom + 1e-9;
Assert.True(apart, $"cells {i} and {j} overlap");
}
}
[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()
{
var sizes = Enumerable.Repeat((10.0, 10.0), 12).ToList();
var wide = DrawingSetPreviewLayout.Arrange(sizes, 1200, 300, 0, 0);
var tall = DrawingSetPreviewLayout.Arrange(sizes, 300, 1200, 0, 0);
Assert.True(wide.Width > wide.Height);
Assert.True(tall.Height > tall.Width);
}
[Fact]
public void Control_ShowsEveryDrawingAtItsOwnSize() => StaTestThread.Run(() =>
{
using var view = new DrawingSetPreviewControl { Size = new System.Drawing.Size(900, 700) };
var drawings = new[]
{
(Rectangle("large", 48, 20, x: 100, y: -50), "large ×1"),
(Rectangle("small", 2, 1), "small ×10"),
(new Drawing("empty"), "empty ×1"),
};
view.ShowDrawings(drawings);
Assert.Equal(2, view.ShownParts.Count);
Assert.Equal(2, view.Plate.Parts.Count);
var large = view.ShownParts[0].BoundingBox;
var small = view.ShownParts[1].BoundingBox;
Assert.Equal((48, 20), (System.Math.Round(large.Length, 6), System.Math.Round(large.Width, 6)));
Assert.Equal((2, 1), (System.Math.Round(small.Length, 6), System.Math.Round(small.Width, 6)));
Assert.True(large.Right <= small.Left || small.Right <= large.Left || large.Top <= small.Bottom || small.Top <= large.Bottom);
Assert.Equal((float)view.PackedLayout.Scale, view.ViewScale, 3);
// Resizing repacks to the new view; sizes never change.
view.Size = new System.Drawing.Size(400, 900);
Assert.Equal(48, System.Math.Round(view.ShownParts[0].BoundingBox.Length, 6));
Assert.Equal((float)view.PackedLayout.Scale, view.ViewScale, 3);
}, TimeSpan.FromMinutes(1), "The STA test did not complete.");
[Fact]
public void Browser_PreviewsTheHighlightedSetBesideTheList() => StaTestThread.Run(() =>
{
var plate = new DrawingLibraryRecord { Id = Guid.NewGuid(), Name = "SYN-PLATE", Revision = 2 };
var gone = new DrawingLibraryRecord { Id = Guid.NewGuid(), Name = "SYN-GONE", Revision = 1 };
var sets = new InMemoryDrawingSets();
Run(sets.CreateAsync(new DrawingSetRecord
{
Id = Guid.NewGuid(),
Name = "Synthetic kit",
Members = new[]
{
new DrawingSetMember { DrawingId = plate.Id, Quantity = 4, Drawing = plate },
new DrawingSetMember { DrawingId = gone.Id, Quantity = 1, Drawing = gone },
},
}));
var requested = new List<(Guid, int)>();
using var form = new System.Windows.Forms.Form { Size = new System.Drawing.Size(1000, 600) };
var browser = new DrawingSetBrowser { Dock = System.Windows.Forms.DockStyle.Fill };
form.Controls.Add(browser);
browser.Initialize(sets, new InMemoryDrawingLibrary(), "http://library.test:8090");
browser.LoadPreviewDrawing = (record, _) =>
{
requested.Add((record.Id, record.Revision));
return Task.FromResult(record == plate ? Rectangle(record.Name, 10, 5) : null);
};
form.Show();
var refresh = browser.RefreshAsync();
PumpUntil(() => refresh.IsCompleted && browser.SelectedSet != null);
// The pane beside the list keeps its width; the list takes the rest.
Assert.InRange(browser.PreviewBox.Width, 300, 440);
PumpUntil(() => browser.PreviewBox.ShownParts.Count == 1 && browser.PreviewLoad.IsCompleted);
Assert.True(browser.PreviewLoad.IsCompletedSuccessfully);
Assert.Equal(new[] { (plate.Id, 2), (gone.Id, 1) }, requested.OrderBy(r => r.Item1 == gone.Id));
Assert.Equal("SYN-PLATE", browser.PreviewBox.ShownParts[0].BaseDrawing.Name);
Assert.Equal(new[] { "2 drawings, 5 parts, one scale", "1 could not be loaded." }, browser.PreviewBox.InfoLines);
// Reading the list again keeps the shown preview and its cached drawings.
refresh = browser.RefreshAsync();
PumpUntil(() => refresh.IsCompleted);
Run(browser.LoadSelectedPreviewAsync());
Assert.Single(browser.PreviewBox.ShownParts);
Assert.Equal(2, requested.Count(r => r.Item1 == gone.Id) + requested.Count(r => r.Item1 == plate.Id));
}, TimeSpan.FromMinutes(1), "The STA test did not complete.");
private static void Run(Task task)
{
PumpUntil(() => task.IsCompleted);
task.GetAwaiter().GetResult();
}
private static void PumpUntil(Func condition)
{
var until = DateTime.UtcNow.AddSeconds(15);
while (!condition() && DateTime.UtcNow < until)
{
System.Windows.Forms.Application.DoEvents();
Thread.Sleep(10);
}
Assert.True(condition(), "Timed out waiting for the browser.");
}
}