Files
OpenNest/OpenNest.WinForms.Tests/Controls/DrawingSetPreviewTests.cs
T
ajandClaude Opus 5.5 73a7c8d6a2 feat(desktop): drop a set's shared name prefix from preview labels
When every drawing in a set starts with the same words, as in
"4980 A111 PT01", the set preview's labels were all cut short to the
same "4980 A111 P…". The preview now drops the leading words all names
share, cutting only at a space, '-', '_' or '.' and never emptying a
name, and notes the dropped prefix above the drawings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 22:39:37 -04:00

236 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using OpenNest.Controls;
using OpenNest.Data;
using OpenNest.Geometry;
using OpenNest.Tests.Data;
namespace OpenNest.WinForms.Tests.Controls;
/// <summary>
/// The set preview packs every drawing into one view at one scale, without overlaps, each in
/// its own orientation, and stays inside the view.
/// </summary>
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.");
[Theory]
[InlineData("4980 A111 PT01|4980 A111 PT12|4980 A111 BRKT", "4980 A111 ")]
[InlineData("4980-A111-P1|4980-A111-P12", "4980-A111-")]
[InlineData("4980 A111|4980 A111 P1", "4980 ")]
[InlineData("SYN 1 |SYN 1 B", "SYN ")]
[InlineData("PLATE|PLATES", "")]
[InlineData("A1|B1", "")]
[InlineData("4980 A111 P1", "")]
[InlineData("4980 A111 P1|", "")]
public void SharedNamePrefix_DropsOnlyWholeLeadingWordsEveryNameShares(string names, string expected) =>
Assert.Equal(expected, DrawingSetBrowser.SharedNamePrefix(names.Split('|')));
[Fact]
public void Browser_LabelsDrawingsWithoutTheirSharedPrefix() => StaTestThread.Run(() =>
{
var first = new DrawingLibraryRecord { Id = Guid.NewGuid(), Name = "SYN A1 PLATE", Revision = 1 };
var second = new DrawingLibraryRecord { Id = Guid.NewGuid(), Name = "SYN A1 GUSSET", Revision = 1 };
var sets = new InMemoryDrawingSets();
Run(sets.CreateAsync(new DrawingSetRecord
{
Id = Guid.NewGuid(),
Name = "Synthetic kit",
Members = new[]
{
new DrawingSetMember { DrawingId = first.Id, Quantity = 2, Drawing = first },
new DrawingSetMember { DrawingId = second.Id, Quantity = 3, Drawing = second },
},
}));
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, _) => Task.FromResult(Rectangle(record.Name, 10, 5));
form.Show();
var refresh = browser.RefreshAsync();
PumpUntil(() => refresh.IsCompleted && browser.SelectedSet != null);
PumpUntil(() => browser.PreviewBox.ShownParts.Count == 2 && browser.PreviewLoad.IsCompleted);
Assert.Equal(new[] { "PLATE ×2", "GUSSET ×3" }, browser.PreviewBox.ShownLabels);
Assert.Contains("Names start with \"SYN A1\".", browser.PreviewBox.InfoLines);
}, 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", "Names start with \"SYN-\".", "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<bool> 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.");
}
}