mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-06 06:02:11 -04:00
feat(ui): preview the highlighted nest's plates in the Open dialog
The Details area of the Database-mode Open dialog now has a plate
preview beside the Plates/Drawings tabs, with a draggable divider. It
draws one plate of the highlighted nest; the arrow buttons below it step
through the plates ("Plate 2 of 5"), and choosing a row on the Plates tab
shows that plate, so the table and the preview stay on the same plate.
The preview is read-only (no selection or drop) and refits when resized.
NestDetails keeps the downloaded nest's plates (PlateLayouts) beside its
rows, so the preview draws the copy already read for the tables with no
second download.
Tests: NestDetails keeps every plate in nest order (fails with the
assignment removed). The Windows form test steps forward with the
button, checks the row follows, and selects a row back.
This commit is contained in:
1 parent
9920aba6eb
commit
da0c54f280
5 files changed
+189
-21
No files matched your search
@@ -17,6 +17,12 @@ public sealed class NestDetails
|
||||
/// <summary>One row per drawing (cutoffs excluded), in nest order.</summary>
|
||||
public IReadOnlyList<NestDrawingDetail> Drawings { get; init; } = Array.Empty<NestDrawingDetail>();
|
||||
|
||||
/// <summary>
|
||||
/// The plates themselves, in the same order as <see cref="Plates"/>, for the plate
|
||||
/// preview. They belong to the downloaded copy of the nest, never to an open document.
|
||||
/// </summary>
|
||||
public IReadOnlyList<Plate> PlateLayouts { get; init; } = Array.Empty<Plate>();
|
||||
|
||||
public static NestDetails FromNest(Nest nest)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(nest);
|
||||
@@ -52,7 +58,13 @@ public sealed class NestDetails
|
||||
Area: drawing.Area))
|
||||
.ToList();
|
||||
|
||||
return new NestDetails { Units = nest.Units, Plates = plates, Drawings = drawings };
|
||||
return new NestDetails
|
||||
{
|
||||
Units = nest.Units,
|
||||
Plates = plates,
|
||||
Drawings = drawings,
|
||||
PlateLayouts = nest.Plates.ToList(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -54,6 +54,23 @@ public class NestDetailsTests
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromNest_KeepsEachPlateForThePreview_InNestOrder()
|
||||
{
|
||||
var nest = new Nest("Job");
|
||||
var first = nest.CreatePlate();
|
||||
var second = nest.CreatePlate();
|
||||
var third = nest.CreatePlate();
|
||||
|
||||
var details = NestDetails.FromNest(nest);
|
||||
|
||||
Assert.Collection(
|
||||
details.PlateLayouts,
|
||||
plate => Assert.Same(first, plate),
|
||||
plate => Assert.Same(second, plate),
|
||||
plate => Assert.Same(third, plate));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromNest_CountsNestedDrawingsAcrossPlateDuplicates_AndSkipsCutoffs()
|
||||
{
|
||||
|
||||
@@ -21,8 +21,8 @@ public class SavedNestsFormTests
|
||||
var first = Record("First job");
|
||||
var second = Record("Second job");
|
||||
var repository = new FakeRepository(first, second);
|
||||
repository.Files[first.Id] = NestFile(plateQuantity: 2, partsOnPlate: 3);
|
||||
repository.Files[second.Id] = NestFile(plateQuantity: 1, partsOnPlate: 1);
|
||||
repository.Files[first.Id] = NestFile((Quantity: 2, Parts: 3));
|
||||
repository.Files[second.Id] = NestFile((Quantity: 1, Parts: 1));
|
||||
|
||||
using var form = new SavedNestsForm(repository, "http://nest-server:5000");
|
||||
form.Show();
|
||||
@@ -56,6 +56,46 @@ public class SavedNestsFormTests
|
||||
"The saved-nest dialog test did not complete.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlatePreview_StepsThroughThePlates_AndFollowsThePlatesTable()
|
||||
{
|
||||
StaTestThread.Run(
|
||||
() =>
|
||||
{
|
||||
var job = Record("Two plates");
|
||||
var repository = new FakeRepository(job);
|
||||
repository.Files[job.Id] = NestFile((Quantity: 1, Parts: 3), (Quantity: 1, Parts: 1));
|
||||
|
||||
using var form = new SavedNestsForm(repository);
|
||||
form.Show();
|
||||
var plates = Field<DataGridView>(form, "platesGrid");
|
||||
var preview = Field<OpenNest.Controls.PlateView>(form, "platePreview");
|
||||
var label = Field<Label>(form, "plateLabel");
|
||||
var previous = Field<Button>(form, "previousPlateButton");
|
||||
var next = Field<Button>(form, "nextPlateButton");
|
||||
|
||||
PumpUntil(() => plates.Rows.Count == 2, "the nest's plates");
|
||||
Assert.True(preview.Visible);
|
||||
Assert.Equal("Plate 1 of 2", label.Text);
|
||||
Assert.Equal(3, preview.Plate.Parts.Count);
|
||||
Assert.False(previous.Enabled);
|
||||
Assert.True(next.Enabled);
|
||||
|
||||
next.PerformClick();
|
||||
Assert.Equal("Plate 2 of 2", label.Text);
|
||||
Assert.Single(preview.Plate.Parts);
|
||||
Assert.Equal(1, plates.CurrentRow!.Index);
|
||||
Assert.True(previous.Enabled);
|
||||
Assert.False(next.Enabled);
|
||||
|
||||
plates.CurrentCell = plates.Rows[0].Cells[0];
|
||||
Assert.Equal("Plate 1 of 2", label.Text);
|
||||
Assert.Equal(3, preview.Plate.Parts.Count);
|
||||
},
|
||||
TestTimeout,
|
||||
"The saved-nest dialog test did not complete.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MissingArchive_ShowsTheErrorInTheDetailsLine_AndEscapeCancels()
|
||||
{
|
||||
@@ -69,6 +109,8 @@ public class SavedNestsFormTests
|
||||
|
||||
PumpUntil(() => status.Text.Contains("no longer exists"), "the missing-nest message");
|
||||
Assert.Equal(0, Field<DataGridView>(form, "platesGrid").Rows.Count);
|
||||
Assert.False(Field<OpenNest.Controls.PlateView>(form, "platePreview").Visible);
|
||||
Assert.Equal("", Field<Label>(form, "plateLabel").Text);
|
||||
|
||||
Assert.True(ProcessKey(form, Keys.Escape));
|
||||
Assert.Equal(DialogResult.Cancel, form.DialogResult);
|
||||
@@ -81,7 +123,7 @@ public class SavedNestsFormTests
|
||||
private static NestRecord Record(string name) =>
|
||||
new() { Id = Guid.NewGuid(), Name = name, SavedAt = DateTime.Now };
|
||||
|
||||
private static byte[] NestFile(int plateQuantity, int partsOnPlate)
|
||||
private static byte[] NestFile(params (int Quantity, int Parts)[] plates)
|
||||
{
|
||||
var nest = new Nest("Job") { Units = Units.Inches };
|
||||
var program = new OpenNest.CNC.Program();
|
||||
@@ -94,11 +136,14 @@ public class SavedNestsFormTests
|
||||
drawing.Quantity.Required = 10;
|
||||
nest.Drawings.Add(drawing);
|
||||
|
||||
var plate = nest.CreatePlate();
|
||||
plate.Size = new OpenNest.Geometry.Size(48, 96);
|
||||
plate.Quantity = plateQuantity;
|
||||
for (var i = 0; i < partsOnPlate; i++)
|
||||
plate.Parts.Add(new Part(drawing, new Vector(i * 12, 0)));
|
||||
foreach (var (quantity, parts) in plates)
|
||||
{
|
||||
var plate = nest.CreatePlate();
|
||||
plate.Size = new OpenNest.Geometry.Size(48, 96);
|
||||
plate.Quantity = quantity;
|
||||
for (var i = 0; i < parts; i++)
|
||||
plate.Parts.Add(new Part(drawing, new Vector(i * 12, 0)));
|
||||
}
|
||||
|
||||
using var stream = new MemoryStream();
|
||||
new NestWriter(nest).Write(stream);
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using OpenNest.Controls;
|
||||
using OpenNest.Data;
|
||||
using OpenNest.IO;
|
||||
using Timer = System.Windows.Forms.Timer;
|
||||
@@ -15,8 +17,8 @@ namespace OpenNest.Forms;
|
||||
/// File > Open (Database mode). The upper list browses saved nests on the shared nest
|
||||
/// server one bounded page at a time; filtering, sorting and paging run on the server
|
||||
/// through <see cref="NestBrowseSession"/>. The lower tabs show the plates and drawings of
|
||||
/// the highlighted nest, read from its archive through <see cref="NestDetailsSession"/>.
|
||||
/// This form only renders their state.
|
||||
/// the highlighted nest, read from its archive through <see cref="NestDetailsSession"/>,
|
||||
/// beside a preview of one of its plates. This form only renders their state.
|
||||
/// </summary>
|
||||
public sealed class SavedNestsForm : Form
|
||||
{
|
||||
@@ -38,6 +40,11 @@ public sealed class SavedNestsForm : Form
|
||||
private readonly DataGridView platesGrid;
|
||||
private readonly DataGridView drawingsGrid;
|
||||
private readonly TabControl detailsTabs;
|
||||
private readonly SplitContainer detailsSplit;
|
||||
private readonly PlateView platePreview;
|
||||
private readonly Button previousPlateButton;
|
||||
private readonly Button nextPlateButton;
|
||||
private readonly Label plateLabel;
|
||||
private readonly Label detailsStatus;
|
||||
private readonly TextBox searchBox;
|
||||
private readonly Timer searchTimer;
|
||||
@@ -48,6 +55,8 @@ public sealed class SavedNestsForm : Form
|
||||
private readonly ToolStripMenuItem openMenuItem;
|
||||
private readonly ToolStripMenuItem deleteMenuItem;
|
||||
private bool deleting;
|
||||
private IReadOnlyList<Plate> previewPlates = Array.Empty<Plate>();
|
||||
private int previewIndex = -1;
|
||||
|
||||
/// <summary>Set to the chosen record's id when the dialog closes with OK.</summary>
|
||||
public Guid SelectedId { get; private set; }
|
||||
@@ -128,6 +137,11 @@ public sealed class SavedNestsForm : Form
|
||||
AddColumn(platesGrid, "Parts", "Parts", 70, typeof(int), alignRight: true);
|
||||
AddColumn(platesGrid, "Drawings", "Drawings", 80, typeof(int), alignRight: true);
|
||||
AddColumn(platesGrid, "Utilization", "Utilization", 90, typeof(double), alignRight: true, format: "P1");
|
||||
platesGrid.SelectionChanged += (_, _) =>
|
||||
{
|
||||
if (platesGrid.CurrentRow is { } row)
|
||||
ShowPlate(row.Index);
|
||||
};
|
||||
|
||||
drawingsGrid = CreateGrid();
|
||||
drawingsGrid.RowHeadersVisible = false;
|
||||
@@ -142,6 +156,46 @@ public sealed class SavedNestsForm : Form
|
||||
detailsTabs.TabPages.Add(CreateTab("Plates", platesGrid));
|
||||
detailsTabs.TabPages.Add(CreateTab("Drawings", drawingsGrid));
|
||||
|
||||
// Preview of one plate beside the tables; the buttons below it step through the plates.
|
||||
platePreview = new PlateView
|
||||
{
|
||||
Dock = DockStyle.Fill,
|
||||
AllowSelect = false,
|
||||
AllowDrop = false,
|
||||
DrawOrigin = false,
|
||||
Cursor = Cursors.Default,
|
||||
TabStop = false,
|
||||
Visible = false,
|
||||
};
|
||||
platePreview.SizeChanged += (_, _) =>
|
||||
{
|
||||
if (platePreview.Visible)
|
||||
platePreview.ZoomToFit();
|
||||
};
|
||||
previousPlateButton = new Button { Text = "\u25C0", Width = 32, Enabled = false, AccessibleName = "Previous plate" };
|
||||
previousPlateButton.Click += (_, _) => ShowPlate(previewIndex - 1);
|
||||
nextPlateButton = new Button { Text = "\u25B6", Width = 32, Enabled = false, AccessibleName = "Next plate" };
|
||||
nextPlateButton.Click += (_, _) => ShowPlate(previewIndex + 1);
|
||||
plateLabel = new Label { Dock = DockStyle.Fill, TextAlign = ContentAlignment.MiddleCenter, AutoEllipsis = true };
|
||||
|
||||
var plateBar = new TableLayoutPanel { Dock = DockStyle.Bottom, AutoSize = true, ColumnCount = 3, RowCount = 1 };
|
||||
plateBar.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
|
||||
plateBar.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
|
||||
plateBar.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
|
||||
plateBar.Controls.Add(previousPlateButton, 0, 0);
|
||||
plateBar.Controls.Add(plateLabel, 1, 0);
|
||||
plateBar.Controls.Add(nextPlateButton, 2, 0);
|
||||
|
||||
detailsSplit = new SplitContainer
|
||||
{
|
||||
Dock = DockStyle.Fill,
|
||||
Orientation = Orientation.Vertical,
|
||||
SplitterWidth = 6,
|
||||
};
|
||||
detailsSplit.Panel1.Controls.Add(detailsTabs);
|
||||
detailsSplit.Panel2.Controls.Add(platePreview);
|
||||
detailsSplit.Panel2.Controls.Add(plateBar);
|
||||
|
||||
detailsStatus = new Label
|
||||
{
|
||||
Dock = DockStyle.Fill,
|
||||
@@ -161,7 +215,7 @@ public sealed class SavedNestsForm : Form
|
||||
};
|
||||
split.Panel1.Controls.Add(nestGrid);
|
||||
split.Panel1.Controls.Add(SectionLabel("Nests", DockStyle.Top));
|
||||
split.Panel2.Controls.Add(detailsTabs);
|
||||
split.Panel2.Controls.Add(detailsSplit);
|
||||
split.Panel2.Controls.Add(detailsHeader);
|
||||
|
||||
// Find bar and paging.
|
||||
@@ -226,6 +280,9 @@ public sealed class SavedNestsForm : Form
|
||||
split.Panel1MinSize = 120;
|
||||
split.Panel2MinSize = 120;
|
||||
split.SplitterDistance = System.Math.Max(split.Panel1MinSize, split.Height * 3 / 5);
|
||||
detailsSplit.Panel1MinSize = 240;
|
||||
detailsSplit.Panel2MinSize = 160;
|
||||
detailsSplit.SplitterDistance = System.Math.Max(detailsSplit.Panel1MinSize, detailsSplit.Width * 3 / 5);
|
||||
};
|
||||
Shown += async (_, _) =>
|
||||
{
|
||||
@@ -518,6 +575,13 @@ public sealed class SavedNestsForm : Form
|
||||
|
||||
private void ShowDetailsMessage(string message)
|
||||
{
|
||||
previewPlates = Array.Empty<Plate>();
|
||||
previewIndex = -1;
|
||||
platePreview.Visible = false;
|
||||
plateLabel.Text = "";
|
||||
previousPlateButton.Enabled = false;
|
||||
nextPlateButton.Enabled = false;
|
||||
|
||||
platesGrid.Rows.Clear();
|
||||
drawingsGrid.Rows.Clear();
|
||||
detailsStatus.Text = message;
|
||||
@@ -531,6 +595,7 @@ public sealed class SavedNestsForm : Form
|
||||
|
||||
ShowDetailsMessage(
|
||||
$"{nestDetails.Plates.Count:N0} plate(s), {nestDetails.Drawings.Count:N0} drawing(s)");
|
||||
previewPlates = nestDetails.PlateLayouts;
|
||||
foreach (var plate in nestDetails.Plates)
|
||||
{
|
||||
platesGrid.Rows.Add(
|
||||
@@ -552,6 +617,32 @@ public sealed class SavedNestsForm : Form
|
||||
drawing.Remaining,
|
||||
drawing.Area);
|
||||
}
|
||||
|
||||
ShowPlate(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows one plate of the highlighted nest in the preview and highlights its row in the
|
||||
/// Plates table. Plate buttons and row selection both come through here.
|
||||
/// </summary>
|
||||
private void ShowPlate(int index)
|
||||
{
|
||||
if (index < 0 || index >= previewPlates.Count)
|
||||
return;
|
||||
|
||||
if (index != previewIndex)
|
||||
{
|
||||
previewIndex = index;
|
||||
platePreview.Plate = previewPlates[index];
|
||||
platePreview.Visible = true;
|
||||
platePreview.ZoomToFit();
|
||||
}
|
||||
|
||||
plateLabel.Text = $"Plate {index + 1:N0} of {previewPlates.Count:N0}";
|
||||
previousPlateButton.Enabled = index > 0;
|
||||
nextPlateButton.Enabled = index < previewPlates.Count - 1;
|
||||
if (index < platesGrid.Rows.Count && platesGrid.CurrentRow?.Index != index)
|
||||
platesGrid.CurrentCell = platesGrid.Rows[index].Cells[0];
|
||||
}
|
||||
|
||||
private void OpenSelected()
|
||||
|
||||
+12
-9
@@ -13,15 +13,18 @@ The desktop app can save nests two ways:
|
||||
Below the list, the **Details** tabs show the highlighted nest's plates
|
||||
(duplicates, size, parts, drawings, utilization) and drawings (required, nested,
|
||||
remaining, area); they are read from that nest's archive, which is downloaded
|
||||
shortly after the highlight stops moving. Enter or a double-click opens the
|
||||
highlighted nest and Esc closes. Save creates a record on first save and updates
|
||||
that same record afterward. Save As creates a new record (a copy). A failed save
|
||||
keeps the previous record association; switching server URLs creates a record on
|
||||
the new server rather than updating an id from the previous one. Delete on the
|
||||
Open dialog's right-click menu permanently removes the highlighted server record
|
||||
after confirmation. A separate **File > Export .nest...** command is always
|
||||
available (in both modes) for producing a local file to share or back up. Export
|
||||
does not change the document's file save path or database record association.
|
||||
shortly after the highlight stops moving. Beside the tabs, a preview draws one
|
||||
plate of the highlighted nest; the arrow buttons below it step through the
|
||||
plates, and selecting a row on the Plates tab shows that plate. Enter or a
|
||||
double-click opens the highlighted nest and Esc closes. Save creates a record on
|
||||
first save and updates that same record afterward. Save As creates a new record
|
||||
(a copy). A failed save keeps the previous record association; switching server
|
||||
URLs creates a record on the new server rather than updating an id from the
|
||||
previous one. Delete on the Open dialog's right-click menu permanently removes
|
||||
the highlighted server record after confirmation. A separate **File > Export
|
||||
.nest...** command is always available (in both modes) for producing a local
|
||||
file to share or back up. Export does not change the document's file save path
|
||||
or database record association.
|
||||
|
||||
The mode and server address are stored per-PC at `%APPDATA%\OpenNest\storage.json`
|
||||
(`OpenNest.Data.NestStorageSettings`), defaulting to File mode so existing
|
||||
|
||||
Reference in new issue
Block a user