From 73a7c8d6a2d3deb9c79d3ff4322a6f9a3189fde2 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Sun, 25 Oct 2026 20:00:29 -0400 Subject: [PATCH] feat(desktop): drop a set's shared name prefix from preview labels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../Controls/DrawingSetPreviewTests.cs | 46 ++++++++++++++++++- OpenNest/Controls/DrawingSetBrowser.cs | 44 ++++++++++++++++-- OpenNest/Controls/DrawingSetPreviewControl.cs | 2 + 3 files changed, 88 insertions(+), 4 deletions(-) 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;