diff --git a/OpenNest.WinForms.Tests/Controls/DrawingSetPreviewTests.cs b/OpenNest.WinForms.Tests/Controls/DrawingSetPreviewTests.cs index 71352f7b..de7df8d6 100644 --- a/OpenNest.WinForms.Tests/Controls/DrawingSetPreviewTests.cs +++ b/OpenNest.WinForms.Tests/Controls/DrawingSetPreviewTests.cs @@ -123,6 +123,49 @@ public class DrawingSetPreviewTests 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(() => { @@ -161,7 +204,8 @@ public class DrawingSetPreviewTests 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); + 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(); diff --git a/OpenNest/Controls/DrawingSetBrowser.cs b/OpenNest/Controls/DrawingSetBrowser.cs index 1444f4c4..1f5e507e 100644 --- a/OpenNest/Controls/DrawingSetBrowser.cs +++ b/OpenNest/Controls/DrawingSetBrowser.cs @@ -330,8 +330,10 @@ namespace OpenNest.Controls } var members = set.Members; - previewBox.ShowDrawings(members.Select((member, i) => (drawings[i], $"{member.Drawing?.Name} ×{member.Quantity}"))); - previewBox.SetInfo(PreviewInfo(members, drawings).ToArray()); + var prefix = SharedNamePrefix(members.Select(m => m.Drawing?.Name).ToList()); + previewBox.ShowDrawings(members.Select((member, i) => + (drawings[i], $"{member.Drawing?.Name?.Substring(prefix.Length)} ×{member.Quantity}"))); + previewBox.SetInfo(PreviewInfo(members, drawings, prefix).ToArray()); previewShown = (listed.Id, listed.Revision); } @@ -373,10 +375,46 @@ namespace OpenNest.Controls return drawings; } - private static IEnumerable PreviewInfo(IReadOnlyList members, Drawing[] drawings) + /// + /// The leading words every name shares, ending at a space, '-', '_' or '.', so labels can + /// drop them. Empty unless there are two or more names and each keeps text after it. + /// + internal static string SharedNamePrefix(IReadOnlyList names) + { + if (names.Count < 2 || names.Any(string.IsNullOrEmpty)) + return ""; + + var length = names.Min(n => n.Length); + foreach (var name in names) + { + var i = 0; + while (i < length && name[i] == names[0][i]) + i++; + length = i; + } + + // Back up to just past the last separator, so no word is split; a name the prefix + // would empty keeps its whole text. + while (length > 0 && !IsNameSeparator(names[0][length - 1])) + length--; + while (length > 0 && names.Any(n => n.Skip(length).All(IsNameSeparator))) + { + length--; + while (length > 0 && !IsNameSeparator(names[0][length - 1])) + length--; + } + + return names[0].Substring(0, length); + } + + private static bool IsNameSeparator(char c) => c is ' ' or '-' or '_' or '.'; + + private static IEnumerable PreviewInfo(IReadOnlyList members, Drawing[] drawings, string prefix) { var parts = members.Sum(m => m.Quantity); yield return $"{members.Count} drawing{(members.Count == 1 ? "" : "s")}, {parts} part{(parts == 1 ? "" : "s")}, one scale"; + if (prefix.Length > 0) + yield return $"Names start with \"{prefix.TrimEnd()}\"."; if (members.Where(m => m.Drawing != null).Select(m => m.Drawing.Units).Distinct().Count() > 1) yield return "Mixes inches and mm: those are not to scale."; var missing = drawings.Count(d => !DrawingSetPreviewControl.CanShow(d)); diff --git a/OpenNest/Controls/DrawingSetPreviewControl.cs b/OpenNest/Controls/DrawingSetPreviewControl.cs index 68647201..e7bbe214 100644 --- a/OpenNest/Controls/DrawingSetPreviewControl.cs +++ b/OpenNest/Controls/DrawingSetPreviewControl.cs @@ -40,6 +40,8 @@ namespace OpenNest.Controls /// The placed parts, in the order they were given. internal IReadOnlyList ShownParts => items.Select(i => i.Part).ToList(); + internal IReadOnlyList ShownLabels => items.Select(i => i.Label).ToList(); + internal DrawingSetPreviewLayout.Result PackedLayout => layout; internal IReadOnlyList InfoLines => infoLines;