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>
This commit is contained in:
ajandClaude Opus 5.5 committed 2026-10-25 22:39:37 -04:00
1 parent 479883e7ac
commit 73a7c8d6a2
3 files changed
+88 -4

No files matched your search

@@ -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();
+41 -3
View File
@@ -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<string> PreviewInfo(IReadOnlyList<DrawingSetMember> members, Drawing[] drawings)
/// <summary>
/// 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.
/// </summary>
internal static string SharedNamePrefix(IReadOnlyList<string> 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<string> PreviewInfo(IReadOnlyList<DrawingSetMember> 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));
@@ -40,6 +40,8 @@ namespace OpenNest.Controls
/// <summary>The placed parts, in the order they were given.</summary>
internal IReadOnlyList<Part> ShownParts => items.Select(i => i.Part).ToList();
internal IReadOnlyList<string> ShownLabels => items.Select(i => i.Label).ToList();
internal DrawingSetPreviewLayout.Result PackedLayout => layout;
internal IReadOnlyList<string> InfoLines => infoLines;