mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-27 00:21:06 -04:00
feat(desktop): preview the highlighted set beside the Sets list
The set preview moves from its own window into a details pane beside the list, as the Drawing Library has, with the set's notes below it. It reads the highlighted set shortly after the selection settles and caches drawings per revision. Parts are drawn as outlines without fill, and the info lines sit above the fitted drawings instead of over them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
fa70713bfc
commit
cbf3b728aa
8 files changed
+366
-307
No files matched your search
@@ -1,7 +1,7 @@
|
||||
using OpenNest.Controls;
|
||||
using OpenNest.Data;
|
||||
using OpenNest.Forms;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Tests.Data;
|
||||
|
||||
namespace OpenNest.WinForms.Tests.Controls;
|
||||
|
||||
@@ -89,56 +89,84 @@ public class DrawingSetPreviewTests
|
||||
|
||||
view.ShowDrawings(drawings);
|
||||
|
||||
Assert.Equal(2, view.Parts.Count);
|
||||
Assert.Equal(2, view.ShownParts.Count);
|
||||
Assert.Equal(2, view.Plate.Parts.Count);
|
||||
var large = view.Parts[0].BoundingBox;
|
||||
var small = view.Parts[1].BoundingBox;
|
||||
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.Layout.Scale, view.ViewScale, 3);
|
||||
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.Parts[0].BoundingBox.Length, 6));
|
||||
Assert.Equal((float)view.Layout.Scale, view.ViewScale, 3);
|
||||
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 Form_LoadsEachMemberAndCountsTheMissingOnes() => StaTestThread.Run(() =>
|
||||
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 set = new DrawingSetRecord
|
||||
var sets = new InMemoryDrawingSets();
|
||||
Run(sets.CreateAsync(new DrawingSetRecord
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = "Synthetic kit",
|
||||
Revision = 3,
|
||||
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 DrawingSetPreviewForm(set, (record, _) =>
|
||||
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.Preview.Size = new System.Drawing.Size(800, 600);
|
||||
var load = form.LoadAsync();
|
||||
};
|
||||
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<bool> condition)
|
||||
{
|
||||
var until = DateTime.UtcNow.AddSeconds(15);
|
||||
while (!load.IsCompleted && DateTime.UtcNow < until)
|
||||
while (!condition() && DateTime.UtcNow < until)
|
||||
{
|
||||
System.Windows.Forms.Application.DoEvents();
|
||||
Thread.Sleep(10);
|
||||
}
|
||||
|
||||
Assert.True(load.IsCompletedSuccessfully);
|
||||
|
||||
Assert.Equal(new[] { (plate.Id, 2), (gone.Id, 1) }, requested.OrderBy(r => r.Item1 == gone.Id));
|
||||
Assert.Single(form.Preview.Parts);
|
||||
Assert.Equal("SYN-PLATE", form.Preview.Parts[0].BaseDrawing.Name);
|
||||
}, TimeSpan.FromMinutes(1), "The STA test did not complete.");
|
||||
Assert.True(condition(), "Timed out waiting for the browser.");
|
||||
}
|
||||
}
|
||||
+81
-35
@@ -34,6 +34,11 @@ namespace OpenNest.Controls
|
||||
savedColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
savedByColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
stateColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
splitContainer = new System.Windows.Forms.SplitContainer();
|
||||
detailsTable = new System.Windows.Forms.TableLayoutPanel();
|
||||
previewPanel = new System.Windows.Forms.Panel();
|
||||
previewBox = new OpenNest.Controls.DrawingSetPreviewControl();
|
||||
previewTimer = new System.Windows.Forms.Timer(components);
|
||||
notesGroup = new System.Windows.Forms.GroupBox();
|
||||
notesBox = new System.Windows.Forms.TextBox();
|
||||
bottomTable = new System.Windows.Forms.TableLayoutPanel();
|
||||
@@ -42,7 +47,6 @@ namespace OpenNest.Controls
|
||||
summaryLabel = new System.Windows.Forms.Label();
|
||||
newButton = new System.Windows.Forms.Button();
|
||||
editButton = new System.Windows.Forms.Button();
|
||||
previewButton = new System.Windows.Forms.Button();
|
||||
moreButton = new System.Windows.Forms.Button();
|
||||
releaseButton = new System.Windows.Forms.Button();
|
||||
statusLabel = new System.Windows.Forms.Label();
|
||||
@@ -53,13 +57,18 @@ namespace OpenNest.Controls
|
||||
editSeparator = new System.Windows.Forms.ToolStripSeparator();
|
||||
archiveMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
historyMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
previewMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
releasesMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
importSeparator = new System.Windows.Forms.ToolStripSeparator();
|
||||
importBomMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
layoutTable.SuspendLayout();
|
||||
filterTable.SuspendLayout();
|
||||
((System.ComponentModel.ISupportInitialize)setGrid).BeginInit();
|
||||
((System.ComponentModel.ISupportInitialize)splitContainer).BeginInit();
|
||||
splitContainer.Panel1.SuspendLayout();
|
||||
splitContainer.Panel2.SuspendLayout();
|
||||
splitContainer.SuspendLayout();
|
||||
detailsTable.SuspendLayout();
|
||||
previewPanel.SuspendLayout();
|
||||
notesGroup.SuspendLayout();
|
||||
bottomTable.SuspendLayout();
|
||||
moreMenu.SuspendLayout();
|
||||
@@ -70,18 +79,16 @@ namespace OpenNest.Controls
|
||||
layoutTable.ColumnCount = 1;
|
||||
layoutTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
|
||||
layoutTable.Controls.Add(filterTable, 0, 0);
|
||||
layoutTable.Controls.Add(setGrid, 0, 1);
|
||||
layoutTable.Controls.Add(notesGroup, 0, 2);
|
||||
layoutTable.Controls.Add(bottomTable, 0, 3);
|
||||
layoutTable.Controls.Add(statusLabel, 0, 4);
|
||||
layoutTable.Controls.Add(splitContainer, 0, 1);
|
||||
layoutTable.Controls.Add(bottomTable, 0, 2);
|
||||
layoutTable.Controls.Add(statusLabel, 0, 3);
|
||||
layoutTable.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
layoutTable.Location = new System.Drawing.Point(0, 0);
|
||||
layoutTable.Name = "layoutTable";
|
||||
layoutTable.Padding = new System.Windows.Forms.Padding(0);
|
||||
layoutTable.RowCount = 5;
|
||||
layoutTable.RowCount = 4;
|
||||
layoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
layoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F));
|
||||
layoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 80F));
|
||||
layoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
layoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
layoutTable.Size = new System.Drawing.Size(904, 541);
|
||||
@@ -202,6 +209,55 @@ namespace OpenNest.Controls
|
||||
stateColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.NotSortable;
|
||||
stateColumn.Width = 80;
|
||||
//
|
||||
// splitContainer
|
||||
//
|
||||
splitContainer.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
splitContainer.FixedPanel = System.Windows.Forms.FixedPanel.Panel2;
|
||||
splitContainer.Margin = new System.Windows.Forms.Padding(0);
|
||||
splitContainer.Name = "splitContainer";
|
||||
splitContainer.Panel1.Controls.Add(setGrid);
|
||||
splitContainer.Panel2.Controls.Add(detailsTable);
|
||||
splitContainer.Size = new System.Drawing.Size(904, 440);
|
||||
splitContainer.SplitterDistance = 480;
|
||||
splitContainer.TabIndex = 1;
|
||||
//
|
||||
// detailsTable
|
||||
//
|
||||
detailsTable.ColumnCount = 1;
|
||||
detailsTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
|
||||
detailsTable.Controls.Add(previewPanel, 0, 0);
|
||||
detailsTable.Controls.Add(notesGroup, 0, 1);
|
||||
detailsTable.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
detailsTable.Margin = new System.Windows.Forms.Padding(0);
|
||||
detailsTable.Name = "detailsTable";
|
||||
detailsTable.Padding = new System.Windows.Forms.Padding(6, 0, 0, 0);
|
||||
detailsTable.RowCount = 2;
|
||||
detailsTable.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F));
|
||||
detailsTable.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 104F));
|
||||
detailsTable.TabIndex = 0;
|
||||
//
|
||||
// previewPanel
|
||||
//
|
||||
previewPanel.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
|
||||
previewPanel.Controls.Add(previewBox);
|
||||
previewPanel.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
previewPanel.Margin = new System.Windows.Forms.Padding(0, 0, 0, 6);
|
||||
previewPanel.MinimumSize = new System.Drawing.Size(0, 160);
|
||||
previewPanel.Name = "previewPanel";
|
||||
previewPanel.TabIndex = 0;
|
||||
//
|
||||
// previewBox
|
||||
//
|
||||
previewBox.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
previewBox.Name = "previewBox";
|
||||
previewBox.TabIndex = 0;
|
||||
previewBox.TabStop = false;
|
||||
//
|
||||
// previewTimer
|
||||
//
|
||||
previewTimer.Interval = 150;
|
||||
previewTimer.Tick += PreviewTimer_Tick;
|
||||
//
|
||||
// notesGroup
|
||||
//
|
||||
notesGroup.Controls.Add(notesBox);
|
||||
@@ -225,7 +281,7 @@ namespace OpenNest.Controls
|
||||
// bottomTable
|
||||
//
|
||||
bottomTable.AutoSize = true;
|
||||
bottomTable.ColumnCount = 8;
|
||||
bottomTable.ColumnCount = 7;
|
||||
bottomTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
|
||||
bottomTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
|
||||
bottomTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
|
||||
@@ -233,15 +289,13 @@ namespace OpenNest.Controls
|
||||
bottomTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
|
||||
bottomTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
|
||||
bottomTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
|
||||
bottomTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
|
||||
bottomTable.Controls.Add(previousButton, 0, 0);
|
||||
bottomTable.Controls.Add(nextButton, 1, 0);
|
||||
bottomTable.Controls.Add(summaryLabel, 2, 0);
|
||||
bottomTable.Controls.Add(newButton, 3, 0);
|
||||
bottomTable.Controls.Add(editButton, 4, 0);
|
||||
bottomTable.Controls.Add(previewButton, 5, 0);
|
||||
bottomTable.Controls.Add(moreButton, 6, 0);
|
||||
bottomTable.Controls.Add(releaseButton, 7, 0);
|
||||
bottomTable.Controls.Add(moreButton, 5, 0);
|
||||
bottomTable.Controls.Add(releaseButton, 6, 0);
|
||||
bottomTable.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
bottomTable.Margin = new System.Windows.Forms.Padding(0, 6, 0, 0);
|
||||
bottomTable.Name = "bottomTable";
|
||||
@@ -294,22 +348,12 @@ namespace OpenNest.Controls
|
||||
editButton.UseVisualStyleBackColor = true;
|
||||
editButton.Click += EditButton_Click;
|
||||
//
|
||||
// previewButton
|
||||
//
|
||||
previewButton.AutoSize = true;
|
||||
previewButton.Enabled = false;
|
||||
previewButton.Name = "previewButton";
|
||||
previewButton.TabIndex = 5;
|
||||
previewButton.Text = "&Preview...";
|
||||
previewButton.UseVisualStyleBackColor = true;
|
||||
previewButton.Click += PreviewButton_Click;
|
||||
//
|
||||
// moreButton
|
||||
//
|
||||
moreButton.AutoSize = true;
|
||||
moreButton.Enabled = false;
|
||||
moreButton.Name = "moreButton";
|
||||
moreButton.TabIndex = 6;
|
||||
moreButton.TabIndex = 5;
|
||||
moreButton.Text = "M&ore ▾";
|
||||
moreButton.UseVisualStyleBackColor = true;
|
||||
moreButton.Click += MoreButton_Click;
|
||||
@@ -321,7 +365,7 @@ namespace OpenNest.Controls
|
||||
releaseButton.Font = new System.Drawing.Font("Segoe UI", 9.75F, System.Drawing.FontStyle.Bold);
|
||||
releaseButton.Margin = new System.Windows.Forms.Padding(12, 3, 3, 3);
|
||||
releaseButton.Name = "releaseButton";
|
||||
releaseButton.TabIndex = 7;
|
||||
releaseButton.TabIndex = 6;
|
||||
releaseButton.Text = "&Add to Release";
|
||||
releaseButton.UseVisualStyleBackColor = true;
|
||||
releaseButton.Visible = false;
|
||||
@@ -343,7 +387,7 @@ namespace OpenNest.Controls
|
||||
//
|
||||
// moreMenu
|
||||
//
|
||||
moreMenu.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { addToReleaseMenuItem, editMenuItem, previewMenuItem, editSeparator, archiveMenuItem, historyMenuItem, releasesMenuItem, importSeparator, importBomMenuItem });
|
||||
moreMenu.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { addToReleaseMenuItem, editMenuItem, editSeparator, archiveMenuItem, historyMenuItem, releasesMenuItem, importSeparator, importBomMenuItem });
|
||||
moreMenu.Name = "moreMenu";
|
||||
moreMenu.Opening += MoreMenu_Opening;
|
||||
//
|
||||
@@ -366,13 +410,6 @@ namespace OpenNest.Controls
|
||||
//
|
||||
editSeparator.Name = "editSeparator";
|
||||
//
|
||||
// previewMenuItem
|
||||
//
|
||||
previewMenuItem.Enabled = false;
|
||||
previewMenuItem.Name = "previewMenuItem";
|
||||
previewMenuItem.Text = "&Preview...";
|
||||
previewMenuItem.Click += PreviewButton_Click;
|
||||
//
|
||||
// archiveMenuItem
|
||||
//
|
||||
archiveMenuItem.Enabled = false;
|
||||
@@ -417,6 +454,12 @@ namespace OpenNest.Controls
|
||||
filterTable.ResumeLayout(false);
|
||||
filterTable.PerformLayout();
|
||||
((System.ComponentModel.ISupportInitialize)setGrid).EndInit();
|
||||
splitContainer.Panel1.ResumeLayout(false);
|
||||
splitContainer.Panel2.ResumeLayout(false);
|
||||
((System.ComponentModel.ISupportInitialize)splitContainer).EndInit();
|
||||
splitContainer.ResumeLayout(false);
|
||||
detailsTable.ResumeLayout(false);
|
||||
previewPanel.ResumeLayout(false);
|
||||
notesGroup.ResumeLayout(false);
|
||||
notesGroup.PerformLayout();
|
||||
bottomTable.ResumeLayout(false);
|
||||
@@ -440,6 +483,11 @@ namespace OpenNest.Controls
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn savedColumn;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn savedByColumn;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn stateColumn;
|
||||
private System.Windows.Forms.SplitContainer splitContainer;
|
||||
private System.Windows.Forms.TableLayoutPanel detailsTable;
|
||||
private System.Windows.Forms.Panel previewPanel;
|
||||
private OpenNest.Controls.DrawingSetPreviewControl previewBox;
|
||||
private System.Windows.Forms.Timer previewTimer;
|
||||
private System.Windows.Forms.GroupBox notesGroup;
|
||||
private System.Windows.Forms.TextBox notesBox;
|
||||
private System.Windows.Forms.TableLayoutPanel bottomTable;
|
||||
@@ -448,7 +496,6 @@ namespace OpenNest.Controls
|
||||
private System.Windows.Forms.Label summaryLabel;
|
||||
private System.Windows.Forms.Button newButton;
|
||||
private System.Windows.Forms.Button editButton;
|
||||
private System.Windows.Forms.Button previewButton;
|
||||
private System.Windows.Forms.Button moreButton;
|
||||
private System.Windows.Forms.Button releaseButton;
|
||||
private System.Windows.Forms.Label statusLabel;
|
||||
@@ -459,7 +506,6 @@ namespace OpenNest.Controls
|
||||
private System.Windows.Forms.ToolStripSeparator editSeparator;
|
||||
private System.Windows.Forms.ToolStripMenuItem archiveMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem historyMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem previewMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem releasesMenuItem;
|
||||
private System.Windows.Forms.ToolStripSeparator importSeparator;
|
||||
private System.Windows.Forms.ToolStripMenuItem importBomMenuItem;
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using OpenNest.Data;
|
||||
@@ -20,15 +21,25 @@ namespace OpenNest.Controls
|
||||
/// shows their history. <see cref="DrawingSetsForm"/> hosts it on its Sets tab beside its
|
||||
/// <see cref="ReleasePanel"/>, and shows a set's Releases on its Releases tab. Once release is
|
||||
/// enabled, a double-click or Enter adds the set to the release (Add to Release does so for
|
||||
/// every selected set); Edit opens the set editor. New Set, Edit, Preview and Add to Release
|
||||
/// are buttons; Add to Release, Edit Set, Preview, Archive/Restore, History and Releases, then
|
||||
/// Import BOM, are in the More menu, which is also the list's right-click menu. Preview shows
|
||||
/// all of a set's drawings in one window at the same scale (<see cref="DrawingSetPreviewForm"/>). In pick mode
|
||||
/// every selected set); Edit opens the set editor. New Set, Edit and Add to Release are
|
||||
/// buttons; Add to Release, Edit Set, Archive/Restore, History and Releases, then Import BOM,
|
||||
/// are in the More menu, which is also the list's right-click menu. The details pane beside
|
||||
/// the list shows the highlighted set's notes and all of its drawings at one scale
|
||||
/// (<see cref="DrawingSetPreviewControl"/>), read shortly after the selection settles; drawings
|
||||
/// are cached per revision for the control's lifetime. In pick mode
|
||||
/// (<see cref="ChooseDrawingSetForm"/>) only active sets are listed, and choosing a set or
|
||||
/// New Set raises <see cref="SetActivated"/> or <see cref="NewSetChosen"/> instead.
|
||||
/// </summary>
|
||||
public partial class DrawingSetBrowser : UserControl
|
||||
{
|
||||
private const int DetailsWidth = 420;
|
||||
private const int PreviewCacheLimit = 256;
|
||||
private const int PreviewConcurrency = 4;
|
||||
|
||||
private readonly Dictionary<(Guid Id, int Revision), Drawing> previewDrawings = new();
|
||||
private CancellationTokenSource previewCancel;
|
||||
private (Guid Id, int Revision)? previewShown;
|
||||
private bool detailsFitted;
|
||||
private IDrawingSetStore sets;
|
||||
private IDrawingLibraryStore library;
|
||||
private string serverUrl;
|
||||
@@ -55,8 +66,16 @@ namespace OpenNest.Controls
|
||||
/// </summary>
|
||||
internal Func<DrawingSetEditSession> ImportBom { get; set; }
|
||||
|
||||
/// <summary>Shows the same-scale preview of a set read with its members; replaced by tests.</summary>
|
||||
internal Action<DrawingSetRecord> ShowPreview { get; set; }
|
||||
/// <summary>
|
||||
/// Reads the drawing a record's package holds for the preview, or null when the
|
||||
/// revision is gone; replaced by tests.
|
||||
/// </summary>
|
||||
internal Func<DrawingLibraryRecord, CancellationToken, Task<Drawing>> LoadPreviewDrawing { get; set; }
|
||||
|
||||
/// <summary>The preview read in flight or last finished; for tests.</summary>
|
||||
internal Task PreviewLoad { get; private set; } = Task.CompletedTask;
|
||||
|
||||
internal DrawingSetPreviewControl PreviewBox => previewBox;
|
||||
|
||||
/// <summary>Adds a set read with its members to the release; set by <see cref="EnableRelease"/>.</summary>
|
||||
internal Action<DrawingSetRecord> AddToRelease { get; private set; }
|
||||
@@ -78,6 +97,31 @@ namespace OpenNest.Controls
|
||||
customerColumn.Tag = DrawingSetSortField.Customer;
|
||||
revisionColumn.Tag = DrawingSetSortField.Revision;
|
||||
savedColumn.Tag = DrawingSetSortField.SavedAt;
|
||||
previewBox.SetInfo("Select a set to preview its drawings.");
|
||||
Load += (_, _) => FitDetailsPane();
|
||||
// Until the first fit takes, every resize retries it; after that the split is the user's.
|
||||
splitContainer.Resize += (_, _) =>
|
||||
{
|
||||
if (!detailsFitted && IsHandleCreated)
|
||||
FitDetailsPane();
|
||||
};
|
||||
Disposed += (_, _) => previewCancel?.Cancel();
|
||||
}
|
||||
|
||||
// The list takes the spare width; the details pane keeps its width as the window resizes.
|
||||
// A container too narrow for both minimums (a tab not yet laid out) is left alone, since
|
||||
// the split container refuses minimums it cannot honor.
|
||||
internal void FitDetailsPane()
|
||||
{
|
||||
const int listMin = 320;
|
||||
const int detailsMin = 300;
|
||||
var available = splitContainer.Width - splitContainer.SplitterWidth;
|
||||
if (available < listMin + detailsMin)
|
||||
return;
|
||||
splitContainer.Panel1MinSize = listMin;
|
||||
splitContainer.SplitterDistance = System.Math.Clamp(available - DetailsWidth, listMin, available - detailsMin);
|
||||
splitContainer.Panel2MinSize = detailsMin;
|
||||
detailsFitted = true;
|
||||
}
|
||||
|
||||
/// <summary>True once <see cref="Initialize"/> has run; the first <see cref="RefreshAsync"/> loads the list.</summary>
|
||||
@@ -135,14 +179,10 @@ namespace OpenNest.Controls
|
||||
return form.ShowDialog(FindForm()) == DialogResult.OK ? form.Edit : null;
|
||||
};
|
||||
setGrid.ContextMenuStrip = pick ? null : moreMenu;
|
||||
ShowPreview = set =>
|
||||
LoadPreviewDrawing = async (record, token) =>
|
||||
{
|
||||
using var form = new DrawingSetPreviewForm(set, async (record, token) =>
|
||||
{
|
||||
var bytes = await library.GetRevisionPackageAsync(record.Id, record.Revision, token);
|
||||
return bytes == null ? null : await Task.Run(() => DrawingPackage.Read(bytes).Drawing, token);
|
||||
});
|
||||
form.ShowDialog(FindForm());
|
||||
var bytes = await library.GetRevisionPackageAsync(record.Id, record.Revision, token);
|
||||
return bytes == null ? null : await Task.Run(() => DrawingPackage.Read(bytes).Drawing, token);
|
||||
};
|
||||
setGrid.MultiSelect = !pick;
|
||||
// Add to Release acts on every selected set.
|
||||
@@ -153,7 +193,6 @@ namespace OpenNest.Controls
|
||||
newButton.Text = "New Set";
|
||||
showArchivedCheckBox.Visible = false;
|
||||
editButton.Visible = false;
|
||||
previewButton.Visible = false;
|
||||
moreButton.Visible = false;
|
||||
}
|
||||
|
||||
@@ -217,10 +256,134 @@ namespace OpenNest.Controls
|
||||
private void SetGrid_CurrentCellChanged(object sender, EventArgs e)
|
||||
{
|
||||
notesBox.Text = SelectedSet?.Notes ?? "";
|
||||
SchedulePreview();
|
||||
UpdateCommands();
|
||||
SelectedSetChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears the preview for a new highlighted set and starts the short timer that reads it,
|
||||
/// so moving through the list does not read every set passed over.
|
||||
/// </summary>
|
||||
private void SchedulePreview()
|
||||
{
|
||||
var set = SelectedSet;
|
||||
if (set != null && previewShown == (set.Id, set.Revision))
|
||||
return;
|
||||
|
||||
previewTimer.Stop();
|
||||
previewCancel?.Cancel();
|
||||
previewShown = null;
|
||||
previewBox.ShowDrawings(Array.Empty<(Drawing, string)>());
|
||||
if (session == null || set == null)
|
||||
previewBox.SetInfo("Select a set to preview its drawings.");
|
||||
else if (set.MemberCount == 0)
|
||||
previewBox.SetInfo("This set has no drawings.");
|
||||
else
|
||||
{
|
||||
previewBox.SetInfo("Loading preview...");
|
||||
previewTimer.Start();
|
||||
}
|
||||
}
|
||||
|
||||
private void PreviewTimer_Tick(object sender, EventArgs e) => PreviewLoad = LoadSelectedPreviewAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Reads the highlighted set with its members, then each member's current drawing (cached
|
||||
/// per revision), and shows them all at one scale. A newer selection cancels it; a
|
||||
/// failure only shows in the preview, as the preview is best-effort.
|
||||
/// </summary>
|
||||
internal async Task LoadSelectedPreviewAsync()
|
||||
{
|
||||
previewTimer.Stop();
|
||||
var listed = SelectedSet;
|
||||
if (session == null || listed == null || listed.MemberCount == 0)
|
||||
return;
|
||||
|
||||
previewCancel?.Cancel();
|
||||
var cancel = previewCancel = new CancellationTokenSource();
|
||||
var token = cancel.Token;
|
||||
DrawingSetRecord set;
|
||||
Drawing[] drawings;
|
||||
try
|
||||
{
|
||||
set = await sets.GetAsync(listed.Id, token);
|
||||
drawings = set == null ? null : await LoadPreviewDrawingsAsync(set.Members, token);
|
||||
}
|
||||
catch (Exception) when (token.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (cancel == previewCancel && !IsDisposed)
|
||||
previewBox.SetInfo("Preview unavailable.", ex.Message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsDisposed || cancel != previewCancel)
|
||||
return;
|
||||
if (set == null)
|
||||
{
|
||||
previewBox.SetInfo("This set is no longer on the server.");
|
||||
return;
|
||||
}
|
||||
|
||||
var members = set.Members;
|
||||
previewBox.ShowDrawings(members.Select((member, i) => (drawings[i], $"{member.Drawing?.Name} ×{member.Quantity}")));
|
||||
previewBox.SetInfo(PreviewInfo(members, drawings).ToArray());
|
||||
previewShown = (listed.Id, listed.Revision);
|
||||
}
|
||||
|
||||
private async Task<Drawing[]> LoadPreviewDrawingsAsync(IReadOnlyList<DrawingSetMember> members, CancellationToken token)
|
||||
{
|
||||
var drawings = new Drawing[members.Count];
|
||||
using var gate = new SemaphoreSlim(PreviewConcurrency);
|
||||
await Task.WhenAll(members.Select(async (member, i) =>
|
||||
{
|
||||
if (member.Drawing is not { } record)
|
||||
return;
|
||||
var key = (record.Id, record.Revision);
|
||||
if (previewDrawings.TryGetValue(key, out var cached))
|
||||
{
|
||||
drawings[i] = cached;
|
||||
return;
|
||||
}
|
||||
|
||||
await gate.WaitAsync(token);
|
||||
try
|
||||
{
|
||||
drawings[i] = await LoadPreviewDrawing(record, token);
|
||||
}
|
||||
catch (Exception) when (!token.IsCancellationRequested)
|
||||
{
|
||||
// One drawing that fails to load is counted as missing; the rest still show.
|
||||
return;
|
||||
}
|
||||
finally
|
||||
{
|
||||
gate.Release();
|
||||
}
|
||||
|
||||
// Continuations resume on the UI thread, so the cache needs no lock.
|
||||
if (previewDrawings.Count >= PreviewCacheLimit)
|
||||
previewDrawings.Clear();
|
||||
previewDrawings[key] = drawings[i];
|
||||
}));
|
||||
return drawings;
|
||||
}
|
||||
|
||||
private static IEnumerable<string> PreviewInfo(IReadOnlyList<DrawingSetMember> members, Drawing[] drawings)
|
||||
{
|
||||
var parts = members.Sum(m => m.Quantity);
|
||||
yield return $"{members.Count} drawing{(members.Count == 1 ? "" : "s")}, {parts} part{(parts == 1 ? "" : "s")}, one scale";
|
||||
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));
|
||||
if (missing > 0)
|
||||
yield return $"{missing} could not be loaded.";
|
||||
}
|
||||
|
||||
private async void SetGrid_CellDoubleClick(object sender, DataGridViewCellEventArgs e)
|
||||
{
|
||||
if (e.RowIndex < 0)
|
||||
@@ -269,8 +432,6 @@ namespace OpenNest.Controls
|
||||
|
||||
private async void EditButton_Click(object sender, EventArgs e) => await EditSelectedAsync();
|
||||
|
||||
private async void PreviewButton_Click(object sender, EventArgs e) => await PreviewSelectedAsync();
|
||||
|
||||
private async void ArchiveMenuItem_Click(object sender, EventArgs e) => await ToggleArchivedAsync();
|
||||
|
||||
private void HistoryMenuItem_Click(object sender, EventArgs e)
|
||||
@@ -370,49 +531,6 @@ namespace OpenNest.Controls
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the selected set (archived or not) with its members' current drawings and
|
||||
/// shows them all in one window at the same scale.
|
||||
/// </summary>
|
||||
internal async Task PreviewSelectedAsync()
|
||||
{
|
||||
var listed = SelectedSet;
|
||||
if (session == null || listed == null || busy || pick)
|
||||
return;
|
||||
|
||||
DrawingSetRecord current;
|
||||
busy = true;
|
||||
UpdateCommands();
|
||||
statusLabel.Text = $"Reading set '{listed.Name}'...";
|
||||
try
|
||||
{
|
||||
current = await sets.GetAsync(listed.Id);
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException || ex is HttpRequestException || ex is TaskCanceledException)
|
||||
{
|
||||
Fail($"Could not read set '{listed.Name}': {ex.Message}");
|
||||
return;
|
||||
}
|
||||
finally
|
||||
{
|
||||
busy = false;
|
||||
if (!IsDisposed)
|
||||
UpdateCommands();
|
||||
}
|
||||
|
||||
if (IsDisposed)
|
||||
return;
|
||||
statusLabel.Text = "";
|
||||
if (current == null)
|
||||
{
|
||||
ShowMessage($"Set '{listed.Name}' no longer exists on the server.", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
await RunAsync(session.RefreshAsync);
|
||||
return;
|
||||
}
|
||||
|
||||
ShowPreview(current);
|
||||
}
|
||||
|
||||
private void ActivateSelected()
|
||||
{
|
||||
if (SelectedSet is { Archived: false })
|
||||
@@ -577,13 +695,12 @@ namespace OpenNest.Controls
|
||||
editButton.Enabled = ready && selected is { Archived: false };
|
||||
editMenuItem.Enabled = editButton.Enabled;
|
||||
editMenuItem.Visible = editSeparator.Visible = !pick;
|
||||
previewButton.Enabled = previewMenuItem.Enabled = ready && !pick && selected is { MemberCount: > 0 };
|
||||
archiveMenuItem.Enabled = ready && selected != null;
|
||||
archiveMenuItem.Text = selected is { Archived: true } ? "&Restore" : "&Archive";
|
||||
historyMenuItem.Enabled = ready && selected != null;
|
||||
releasesMenuItem.Enabled = ready && selected != null && ShowReleases != null;
|
||||
importBomMenuItem.Enabled = ready && !pick;
|
||||
moreButton.Enabled = previewMenuItem.Enabled || archiveMenuItem.Enabled || historyMenuItem.Enabled || releasesMenuItem.Enabled
|
||||
moreButton.Enabled = archiveMenuItem.Enabled || historyMenuItem.Enabled || releasesMenuItem.Enabled
|
||||
|| importBomMenuItem.Enabled;
|
||||
releaseButton.Enabled = ready && AddToRelease != null
|
||||
&& SelectedSets().Any(s => !s.Archived && s.MemberCount > 0);
|
||||
|
||||
@@ -8,10 +8,10 @@ using OpenNest.Geometry;
|
||||
namespace OpenNest.Controls
|
||||
{
|
||||
/// <summary>
|
||||
/// Shows every drawing of a set side by side at one scale, packed by
|
||||
/// <see cref="DrawingSetPreviewLayout"/> with a label under each. The layout is repacked
|
||||
/// to fit whenever the control is resized; the wheel zooms, the middle button pans and a
|
||||
/// middle double-click fits the set again.
|
||||
/// Shows every drawing of a set side by side at one scale, outlined without fill, packed by
|
||||
/// <see cref="DrawingSetPreviewLayout"/> with a label under each and the info lines above.
|
||||
/// The layout is repacked to fit whenever the control is resized; the wheel zooms, the
|
||||
/// middle button pans and a middle double-click fits the set again.
|
||||
/// </summary>
|
||||
public class DrawingSetPreviewControl : PlateView
|
||||
{
|
||||
@@ -19,13 +19,12 @@ namespace OpenNest.Controls
|
||||
|
||||
private readonly List<(Part Part, string Label)> items = new();
|
||||
private DrawingSetPreviewLayout.Result layout;
|
||||
private string[] infoLines;
|
||||
private string[] infoLines = System.Array.Empty<string>();
|
||||
|
||||
public DrawingSetPreviewControl()
|
||||
{
|
||||
DrawOrigin = false;
|
||||
DrawBounds = false;
|
||||
DrawPartNumbers = false;
|
||||
AllowPan = true;
|
||||
AllowSelect = false;
|
||||
AllowZoom = true;
|
||||
@@ -35,15 +34,25 @@ namespace OpenNest.Controls
|
||||
ViewScaleMin = 1e-5f;
|
||||
}
|
||||
|
||||
/// <summary>False for a drawing with no geometry to show; <see cref="ShowDrawings"/> skips it.</summary>
|
||||
public static bool CanShow(Drawing drawing) => drawing?.Program?.Codes.Count > 0;
|
||||
|
||||
/// <summary>The placed parts, in the order they were given.</summary>
|
||||
internal IReadOnlyList<Part> Parts => items.Select(i => i.Part).ToList();
|
||||
internal IReadOnlyList<Part> ShownParts => items.Select(i => i.Part).ToList();
|
||||
|
||||
internal DrawingSetPreviewLayout.Result Layout => layout;
|
||||
internal DrawingSetPreviewLayout.Result PackedLayout => layout;
|
||||
|
||||
internal IReadOnlyList<string> InfoLines => infoLines;
|
||||
|
||||
/// <summary>Lines shown above the drawings; the drawings are fitted below them.</summary>
|
||||
public void SetInfo(params string[] lines)
|
||||
{
|
||||
infoLines = lines;
|
||||
Invalidate();
|
||||
var before = InfoHeight;
|
||||
infoLines = lines ?? System.Array.Empty<string>();
|
||||
if (InfoHeight != before && items.Count > 0)
|
||||
Arrange();
|
||||
else
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
/// <summary>Replaces the shown drawings; each is labeled with its text.</summary>
|
||||
@@ -66,9 +75,6 @@ namespace OpenNest.Controls
|
||||
Arrange();
|
||||
}
|
||||
|
||||
/// <summary>False for a drawing with no geometry to show; <see cref="ShowDrawings"/> skips it.</summary>
|
||||
public static bool CanShow(Drawing drawing) => drawing?.Program?.Codes.Count > 0;
|
||||
|
||||
public override void ZoomToFit(bool redraw = true)
|
||||
{
|
||||
if (layout == null)
|
||||
@@ -78,7 +84,14 @@ namespace OpenNest.Controls
|
||||
return;
|
||||
}
|
||||
|
||||
ZoomToArea(0, -layout.Height, layout.Width, layout.Height, redraw);
|
||||
// Centers the layout below the info lines; world Y 0 is the layout's top edge.
|
||||
var top = InfoHeight;
|
||||
ViewScale = (float)layout.Scale;
|
||||
origin.X = (Width - LengthWorldToGui(layout.Width)) * 0.5f;
|
||||
origin.Y = top + (Height - top - LengthWorldToGui(layout.Height)) * 0.5f;
|
||||
UpdateMatrix();
|
||||
if (redraw)
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
protected override void OnResize(System.EventArgs e)
|
||||
@@ -88,17 +101,21 @@ namespace OpenNest.Controls
|
||||
Arrange();
|
||||
}
|
||||
|
||||
private int InfoHeight => infoLines.Count(l => !string.IsNullOrEmpty(l)) * (Font.Height + 1) + 4;
|
||||
|
||||
private void Arrange()
|
||||
{
|
||||
if (items.Count == 0 || Width <= BorderWidth || Height <= BorderWidth)
|
||||
var top = InfoHeight;
|
||||
if (items.Count == 0 || Width <= BorderWidth || Height <= BorderWidth + top)
|
||||
{
|
||||
layout = null;
|
||||
Invalidate();
|
||||
return;
|
||||
}
|
||||
|
||||
var sizes = items.Select(i => (i.Part.BoundingBox.Length, i.Part.BoundingBox.Width)).ToList();
|
||||
var labelPixels = Font.Height + 2;
|
||||
layout = DrawingSetPreviewLayout.Arrange(sizes, Width - BorderWidth, Height - BorderWidth, labelPixels, MinCellPixels);
|
||||
layout = DrawingSetPreviewLayout.Arrange(sizes, Width - BorderWidth, Height - BorderWidth - top, labelPixels, MinCellPixels);
|
||||
|
||||
foreach (var placement in layout.Placements)
|
||||
{
|
||||
@@ -119,14 +136,28 @@ namespace OpenNest.Controls
|
||||
e.Graphics.SmoothingMode = SmoothingMode.HighQuality;
|
||||
|
||||
e.Graphics.TranslateTransform(origin.X, origin.Y);
|
||||
Renderer.DrawPlate(e.Graphics);
|
||||
Renderer.DrawParts(e.Graphics);
|
||||
PaintOutlines(e.Graphics);
|
||||
PaintLabels(e.Graphics);
|
||||
e.Graphics.ResetTransform();
|
||||
|
||||
PaintInfo(e.Graphics);
|
||||
}
|
||||
|
||||
// Outlines only: cuts in black, etch marks dashed in gray.
|
||||
private void PaintOutlines(Graphics g)
|
||||
{
|
||||
using var etchPen = new Pen(Color.Gray) { DashStyle = DashStyle.Dash };
|
||||
foreach (var part in LayoutParts)
|
||||
{
|
||||
if (part.IsDirty)
|
||||
part.Update(this);
|
||||
if (part.EtchPath?.PointCount > 0)
|
||||
g.DrawPath(etchPen, part.EtchPath);
|
||||
if (part.Path?.PointCount > 0)
|
||||
g.DrawPath(Pens.Black, part.Path);
|
||||
}
|
||||
}
|
||||
|
||||
private void PaintLabels(Graphics g)
|
||||
{
|
||||
if (layout == null)
|
||||
@@ -151,17 +182,19 @@ namespace OpenNest.Controls
|
||||
|
||||
private void PaintInfo(Graphics g)
|
||||
{
|
||||
if (infoLines == null)
|
||||
return;
|
||||
|
||||
var lineHeight = Font.GetHeight(g) + 1;
|
||||
var lineHeight = Font.Height + 1;
|
||||
var y = 4f;
|
||||
using var format = new StringFormat
|
||||
{
|
||||
Trimming = StringTrimming.EllipsisCharacter,
|
||||
FormatFlags = StringFormatFlags.NoWrap,
|
||||
};
|
||||
|
||||
foreach (var line in infoLines)
|
||||
{
|
||||
if (string.IsNullOrEmpty(line))
|
||||
continue;
|
||||
g.DrawString(line, Font, Brushes.Black, 4, y);
|
||||
g.DrawString(line, Font, Brushes.Black, new RectangleF(4, y, System.Math.Max(Width - 8, 1), lineHeight), format);
|
||||
y += lineHeight;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ namespace OpenNest.Controls
|
||||
|| view.MutedParts?.Contains(part.BasePart) == true;
|
||||
var number = view.PreviewSequence != null
|
||||
&& view.PreviewSequence.TryGetValue(part.BasePart, out var preview) ? preview : i + 1;
|
||||
part.Draw(g, view.DrawPartNumbers ? number.ToString() : null, muted);
|
||||
part.Draw(g, number.ToString(), muted);
|
||||
DrawBendLines(g, part.BasePart, muted);
|
||||
DrawEtchMarks(g, part.BasePart, muted);
|
||||
DrawGrainWarning(g, part.BasePart, muted);
|
||||
|
||||
@@ -191,9 +191,6 @@ namespace OpenNest.Controls
|
||||
|
||||
public bool DrawBounds { get; set; }
|
||||
|
||||
/// <summary>Numbers each part with its position in the plate's sequence.</summary>
|
||||
public bool DrawPartNumbers { get; set; } = true;
|
||||
|
||||
public bool DrawOffset { get; set; }
|
||||
|
||||
public bool DrawCutDirection { get; set; }
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using OpenNest.Controls;
|
||||
using OpenNest.Data;
|
||||
|
||||
namespace OpenNest.Forms
|
||||
{
|
||||
/// <summary>
|
||||
/// Shows every drawing of a set in one <see cref="DrawingSetPreviewControl"/>, all at the
|
||||
/// same scale, labeled with its name and the set's quantity. Each member's current
|
||||
/// revision is downloaded when the window is shown; members that are gone or fail to
|
||||
/// load are counted in the header and left out. Closing cancels the downloads.
|
||||
/// </summary>
|
||||
public class DrawingSetPreviewForm : Form
|
||||
{
|
||||
private const int Concurrency = 4;
|
||||
|
||||
private readonly DrawingSetRecord set;
|
||||
private readonly Func<DrawingLibraryRecord, CancellationToken, Task<Drawing>> load;
|
||||
private readonly CancellationTokenSource cancel = new();
|
||||
private readonly DrawingSetPreviewControl preview;
|
||||
|
||||
/// <param name="set">The set read with its members' current drawings.</param>
|
||||
/// <param name="load">Reads the drawing a record's package holds, or null when the revision is gone.</param>
|
||||
public DrawingSetPreviewForm(DrawingSetRecord set, Func<DrawingLibraryRecord, CancellationToken, Task<Drawing>> load)
|
||||
{
|
||||
this.set = set ?? throw new ArgumentNullException(nameof(set));
|
||||
this.load = load ?? throw new ArgumentNullException(nameof(load));
|
||||
|
||||
preview = new DrawingSetPreviewControl { Dock = DockStyle.Fill };
|
||||
var closeButton = new Button
|
||||
{
|
||||
Text = "Close",
|
||||
DialogResult = DialogResult.Cancel,
|
||||
AutoSize = true,
|
||||
Anchor = AnchorStyles.Right,
|
||||
};
|
||||
var bottom = new FlowLayoutPanel
|
||||
{
|
||||
Dock = DockStyle.Bottom,
|
||||
AutoSize = true,
|
||||
FlowDirection = FlowDirection.RightToLeft,
|
||||
Padding = new Padding(6),
|
||||
};
|
||||
bottom.Controls.Add(closeButton);
|
||||
Controls.Add(preview);
|
||||
Controls.Add(bottom);
|
||||
|
||||
CancelButton = closeButton;
|
||||
Font = new Font("Segoe UI", 9.75F);
|
||||
Text = $"Preview — {set.Name} (revision {set.Revision})";
|
||||
StartPosition = FormStartPosition.CenterParent;
|
||||
Size = new System.Drawing.Size(1100, 800);
|
||||
MinimumSize = new System.Drawing.Size(400, 300);
|
||||
ShowIcon = false;
|
||||
ShowInTaskbar = false;
|
||||
}
|
||||
|
||||
internal DrawingSetPreviewControl Preview => preview;
|
||||
|
||||
/// <summary>Completes once every member has been read and shown; for tests.</summary>
|
||||
internal Task Loaded { get; private set; } = Task.CompletedTask;
|
||||
|
||||
protected override void OnShown(EventArgs e)
|
||||
{
|
||||
base.OnShown(e);
|
||||
Loaded = LoadAsync();
|
||||
}
|
||||
|
||||
protected override void OnFormClosed(FormClosedEventArgs e)
|
||||
{
|
||||
cancel.Cancel();
|
||||
base.OnFormClosed(e);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
cancel.Dispose();
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
internal async Task LoadAsync()
|
||||
{
|
||||
var members = set.Members;
|
||||
if (members.Count == 0)
|
||||
{
|
||||
preview.SetInfo("This set has no drawings.");
|
||||
return;
|
||||
}
|
||||
|
||||
var token = cancel.Token;
|
||||
var drawings = new Drawing[members.Count];
|
||||
var failed = 0;
|
||||
preview.SetInfo($"Loading {members.Count} drawings...");
|
||||
|
||||
using var gate = new SemaphoreSlim(Concurrency);
|
||||
var reads = members.Select(async (member, i) =>
|
||||
{
|
||||
if (member.Drawing == null)
|
||||
{
|
||||
Interlocked.Increment(ref failed);
|
||||
return;
|
||||
}
|
||||
|
||||
await gate.WaitAsync(token);
|
||||
try
|
||||
{
|
||||
drawings[i] = await load(member.Drawing, token);
|
||||
if (!DrawingSetPreviewControl.CanShow(drawings[i]))
|
||||
Interlocked.Increment(ref failed);
|
||||
}
|
||||
catch (Exception) when (!token.IsCancellationRequested)
|
||||
{
|
||||
// The preview is best-effort: a drawing that fails to load is counted and skipped.
|
||||
Interlocked.Increment(ref failed);
|
||||
}
|
||||
finally
|
||||
{
|
||||
gate.Release();
|
||||
}
|
||||
}).ToList();
|
||||
|
||||
try
|
||||
{
|
||||
await Task.WhenAll(reads);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsDisposed || token.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
preview.ShowDrawings(members.Select((member, i) => (drawings[i], Label(member))));
|
||||
preview.SetInfo(Header(members, failed).ToArray());
|
||||
}
|
||||
|
||||
private static string Label(DrawingSetMember member) =>
|
||||
$"{member.Drawing?.Name} ×{member.Quantity}";
|
||||
|
||||
private IEnumerable<string> Header(IReadOnlyList<DrawingSetMember> members, int failed)
|
||||
{
|
||||
var parts = members.Sum(m => m.Quantity);
|
||||
yield return $"{members.Count} drawings, {parts} parts — all shown at the same scale";
|
||||
|
||||
var units = members.Where(m => m.Drawing != null).Select(m => m.Drawing.Units).Distinct().ToList();
|
||||
if (units.Count > 1)
|
||||
yield return "This set mixes inches and millimeters; drawings in different units are not to scale with each other.";
|
||||
if (failed > 0)
|
||||
yield return $"{failed} drawing{(failed == 1 ? "" : "s")} could not be loaded and {(failed == 1 ? "is" : "are")} not shown.";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -118,8 +118,6 @@ namespace OpenNest
|
||||
internal void Draw(Graphics g, string id, bool muted)
|
||||
{
|
||||
Draw(g, muted);
|
||||
if (id == null)
|
||||
return;
|
||||
|
||||
using var sf = new StringFormat
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user