mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-10 08:44:25 -04:00
fix(ui): exclude the new-plate sentinel from display counts
The editor header and the Plan Cutting dialog counted the trailing empty new-plate workspace PlateManager.EnsureSentinel maintains, so a one-plate nest read 'Plate 1 of 2'. PlateDisplayNumbering now derives display-only numbers that exclude only a trailing empty sentinel; interior empty plates keep their slot and number, and navigation, storage indexes, exported names and batch selection are untouched. The sentinel itself is labeled 'New plate (empty)' instead of being numbered beyond the shown total.
This commit is contained in:
1 parent
bb43118451
commit
b8f319d3e8
6 files changed
+336
-17
No files matched your search
@@ -0,0 +1,64 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest
|
||||
{
|
||||
/// <summary>
|
||||
/// Display-only plate numbering for labels. The editor keeps one trailing empty plate as the
|
||||
/// "new plate" workspace (PlateManager.EnsureSentinel), and that sentinel is excluded from the
|
||||
/// shown total so a one-plate nest reads "Plate 1 of 1". Navigation, storage indexes, exported
|
||||
/// names and batch selection keep using the real collection positions; an interior empty plate
|
||||
/// keeps its slot and its number.
|
||||
/// </summary>
|
||||
public static class PlateDisplayNumbering
|
||||
{
|
||||
/// <summary>
|
||||
/// The number shown for the plate at <paramref name="storageIndex"/>: real collection
|
||||
/// positions count as 1-based plate numbers. Returns null for the trailing "new plate"
|
||||
/// sentinel and for indexes outside the collection.
|
||||
/// </summary>
|
||||
public static int? DisplayedPlateNumber(IList<Plate> plates, int storageIndex)
|
||||
{
|
||||
if (plates == null || storageIndex < 0 || storageIndex >= plates.Count)
|
||||
return null;
|
||||
if (IsTrailingSentinel(plates, storageIndex))
|
||||
return null;
|
||||
return storageIndex + 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The number shown in an "of M" total: the plates minus only a trailing empty sentinel.
|
||||
/// An all-empty collection shows nothing (the caller says "No plates").
|
||||
/// </summary>
|
||||
public static int DisplayedPlateCount(IList<Plate> plates)
|
||||
{
|
||||
if (plates == null || plates.Count == 0)
|
||||
return 0;
|
||||
var last = plates[plates.Count - 1];
|
||||
return plates.Count - (last != null && last.Parts.Count == 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
/// <summary>True when <paramref name="storageIndex"/> is the trailing empty new-plate sentinel.</summary>
|
||||
public static bool IsTrailingSentinel(IList<Plate> plates, int storageIndex)
|
||||
{
|
||||
if (plates == null || plates.Count == 0)
|
||||
return false;
|
||||
if (storageIndex != plates.Count - 1)
|
||||
return false;
|
||||
var plate = plates[storageIndex];
|
||||
return plate != null && plate.Parts.Count == 0;
|
||||
}
|
||||
|
||||
/// <summary>Header text for the plate at <paramref name="storageIndex"/>.</summary>
|
||||
public static string FormatHeader(IList<Plate> plates, int storageIndex, string plateSizeText)
|
||||
{
|
||||
var displayed = DisplayedPlateNumber(plates, storageIndex);
|
||||
if (displayed == null)
|
||||
return IsTrailingSentinel(plates, storageIndex)
|
||||
? "New plate (empty)"
|
||||
: "No plates";
|
||||
return string.IsNullOrEmpty(plateSizeText)
|
||||
? string.Format("Plate {0} of {1}", displayed.Value, DisplayedPlateCount(plates))
|
||||
: string.Format("Plate {0} of {1} | {2}", displayed.Value, DisplayedPlateCount(plates), plateSizeText);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Tests;
|
||||
|
||||
public class PlateDisplayNumberingTests
|
||||
{
|
||||
private static Nest CreateNest() => new("test");
|
||||
|
||||
private static Part MakePart()
|
||||
{
|
||||
var pgm = new Program();
|
||||
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(10, 0)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(10, 10)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(0, 0)));
|
||||
return new Part(new Drawing("test", pgm));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PopulatedThenSentinel_TotalIsOneAndRealPlateDisplaysAsOne()
|
||||
{
|
||||
var nest = CreateNest();
|
||||
var real = nest.CreatePlate();
|
||||
real.Parts.Add(MakePart());
|
||||
nest.CreatePlate(); // The trailing empty sentinel.
|
||||
|
||||
Assert.Equal(1, PlateDisplayNumbering.DisplayedPlateCount(nest.Plates));
|
||||
Assert.Equal(1, PlateDisplayNumbering.DisplayedPlateNumber(nest.Plates, 0));
|
||||
Assert.Equal("Plate 1 of 1", PlateDisplayNumbering.FormatHeader(nest.Plates, 0, null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sentinel_IsLabeledAsNewPlateNotNumbered()
|
||||
{
|
||||
var nest = CreateNest();
|
||||
var real = nest.CreatePlate();
|
||||
real.Parts.Add(MakePart());
|
||||
nest.CreatePlate();
|
||||
|
||||
var sentinel = nest.Plates.Count - 1;
|
||||
Assert.True(PlateDisplayNumbering.IsTrailingSentinel(nest.Plates, sentinel));
|
||||
Assert.Null(PlateDisplayNumbering.DisplayedPlateNumber(nest.Plates, sentinel));
|
||||
Assert.Equal("New plate (empty)", PlateDisplayNumbering.FormatHeader(nest.Plates, sentinel, null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TwoRealPlatesThenSentinel_TotalIsTwo()
|
||||
{
|
||||
var nest = CreateNest();
|
||||
nest.Plates.Add(WithPart());
|
||||
nest.Plates.Add(WithPart());
|
||||
nest.CreatePlate(); // sentinel
|
||||
|
||||
Assert.Equal(2, PlateDisplayNumbering.DisplayedPlateCount(nest.Plates));
|
||||
Assert.Equal(1, PlateDisplayNumbering.DisplayedPlateNumber(nest.Plates, 0));
|
||||
Assert.Equal(2, PlateDisplayNumbering.DisplayedPlateNumber(nest.Plates, 1));
|
||||
Assert.Null(PlateDisplayNumbering.DisplayedPlateNumber(nest.Plates, 2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NoPlates_TotalIsZeroAndHeaderSaysNoPlates()
|
||||
{
|
||||
var nest = CreateNest();
|
||||
|
||||
Assert.Equal(0, PlateDisplayNumbering.DisplayedPlateCount(nest.Plates));
|
||||
Assert.Null(PlateDisplayNumbering.DisplayedPlateNumber(nest.Plates, 0));
|
||||
Assert.Equal("No plates", PlateDisplayNumbering.FormatHeader(nest.Plates, 0, null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SingleEmptyPlate_IsTheNewPlateView_NotNumbered()
|
||||
{
|
||||
var nest = CreateNest();
|
||||
nest.CreatePlate(); // only the sentinel
|
||||
|
||||
Assert.Equal(0, PlateDisplayNumbering.DisplayedPlateCount(nest.Plates));
|
||||
Assert.True(PlateDisplayNumbering.IsTrailingSentinel(nest.Plates, 0));
|
||||
Assert.Equal("New plate (empty)", PlateDisplayNumbering.FormatHeader(nest.Plates, 0, null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InteriorEmptyPlate_KeepsItsSlotAndNumber()
|
||||
{
|
||||
var nest = CreateNest();
|
||||
nest.Plates.Add(WithPart());
|
||||
nest.CreatePlate(); // An empty interior plate: EnsureSentinel only trims at the tail.
|
||||
nest.Plates.Add(WithPart());
|
||||
nest.CreatePlate(); // sentinel
|
||||
|
||||
Assert.Equal(3, PlateDisplayNumbering.DisplayedPlateCount(nest.Plates));
|
||||
Assert.Equal(2, PlateDisplayNumbering.DisplayedPlateNumber(nest.Plates, 1));
|
||||
Assert.Equal(3, PlateDisplayNumbering.DisplayedPlateNumber(nest.Plates, 2));
|
||||
Assert.False(PlateDisplayNumbering.IsTrailingSentinel(nest.Plates, 1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LastPlateWithParts_IsNotASentinel()
|
||||
{
|
||||
var nest = CreateNest();
|
||||
nest.Plates.Add(WithPart());
|
||||
|
||||
Assert.Equal(1, PlateDisplayNumbering.DisplayedPlateCount(nest.Plates));
|
||||
Assert.False(PlateDisplayNumbering.IsTrailingSentinel(nest.Plates, 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IndexOutsideCollection_HasNoNumberAndIsNotASentinel()
|
||||
{
|
||||
var nest = CreateNest();
|
||||
nest.Plates.Add(WithPart());
|
||||
|
||||
Assert.Null(PlateDisplayNumbering.DisplayedPlateNumber(nest.Plates, 5));
|
||||
Assert.Null(PlateDisplayNumbering.DisplayedPlateNumber(nest.Plates, -1));
|
||||
Assert.False(PlateDisplayNumbering.IsTrailingSentinel(nest.Plates, 5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Helper_DoesNotMutateTheNest()
|
||||
{
|
||||
var nest = CreateNest();
|
||||
nest.Plates.Add(WithPart());
|
||||
nest.CreatePlate();
|
||||
var before = nest.Plates.Count;
|
||||
|
||||
PlateDisplayNumbering.DisplayedPlateCount(nest.Plates);
|
||||
PlateDisplayNumbering.FormatHeader(nest.Plates, 1, "100 x 100");
|
||||
|
||||
Assert.Equal(before, nest.Plates.Count);
|
||||
}
|
||||
|
||||
private static Plate WithPart()
|
||||
{
|
||||
var plate = new Plate();
|
||||
plate.Parts.Add(MakePart());
|
||||
return plate;
|
||||
}
|
||||
}
|
||||
@@ -202,6 +202,31 @@ public class CuttingPlanFormTests
|
||||
Assert.All(first.Parts, part => Assert.False(part.HasManualLeadIns));
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void OnePlateDialog_ExcludesTheSentinelFromTheShownTotal() => RunSta(() =>
|
||||
{
|
||||
var (nest, view) = CreateView(Square("a", 1, 1));
|
||||
nest.CreatePlate(); // The editor's trailing empty new-plate sentinel.
|
||||
using var editor = view;
|
||||
using var form = new CuttingPlanForm(view, nest, allPlates: false, Parameters());
|
||||
|
||||
Assert.StartsWith("Plate 1 of 1, numbered in cutting order:", Control<Label>(form, "previewLabel").Text);
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void SentinelActivePlate_IsLabeledNotNumberedBeyondTheTotal() => RunSta(() =>
|
||||
{
|
||||
var (nest, _) = CreateView(Square("a", 1, 1));
|
||||
var sentinel = nest.CreatePlate(); // trailing empty new-plate sentinel
|
||||
using var view = new PlateView { Plate = sentinel };
|
||||
using var editor = view;
|
||||
using var form = new CuttingPlanForm(view, nest, allPlates: false, Parameters());
|
||||
|
||||
var text = Control<Label>(form, "previewLabel").Text;
|
||||
Assert.StartsWith("New plate (empty)", text);
|
||||
Assert.DoesNotContain("Plate 2", text);
|
||||
});
|
||||
|
||||
private static (Nest Nest, PlateView View) CreateView(params Part[] parts)
|
||||
{
|
||||
var nest = new Nest();
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
using System.Reflection;
|
||||
using System.Windows.Forms;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Forms;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.WinForms.Tests.Forms;
|
||||
|
||||
public class PlateHeaderNumberingTests
|
||||
{
|
||||
[Fact]
|
||||
public void OnePopulatedPlateWithSentinel_HeaderSaysOneOfOne() => RunSta(() =>
|
||||
{
|
||||
var nest = new Nest("one plate");
|
||||
var plate = nest.CreatePlate();
|
||||
plate.Parts.Add(Square("a"));
|
||||
using var form = new EditNestForm(nest);
|
||||
form.PlateView.SetOverlapAutoCheck(null); // Background overlap workers are irrelevant here.
|
||||
form.Show();
|
||||
|
||||
// The constructor's EnsureSentinel keeps the trailing empty new-plate workspace.
|
||||
Assert.Equal(2, nest.Plates.Count);
|
||||
Assert.Equal("Plate 1 of 1", Header(form).Split(" | ")[0]);
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void NavigatingOntoTheSentinelAndBack_RelabelsWithoutTouchingNavigation() => RunSta(() =>
|
||||
{
|
||||
var nest = new Nest("sentinel navigation");
|
||||
var plate = nest.CreatePlate();
|
||||
plate.Parts.Add(Square("a"));
|
||||
using var form = new EditNestForm(nest);
|
||||
form.PlateView.SetOverlapAutoCheck(null);
|
||||
form.Show();
|
||||
|
||||
Assert.True(form.PlateManager.LoadNext()); // onto the sentinel
|
||||
Assert.Equal("New plate (empty)", Header(form));
|
||||
Assert.Equal(1, form.PlateManager.CurrentIndex); // navigation itself is unchanged
|
||||
|
||||
Assert.True(form.PlateManager.LoadPrevious());
|
||||
Assert.Equal("Plate 1 of 1", Header(form).Split(" | ")[0]);
|
||||
Assert.Equal(2, nest.Plates.Count); // the collection was never mutated for display
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void SingleEmptyPlate_EditorShowsTheNewPlateView() => RunSta(() =>
|
||||
{
|
||||
var nest = new Nest("empty");
|
||||
using var form = new EditNestForm(nest); // EnsureSentinel creates the single empty plate.
|
||||
form.PlateView.SetOverlapAutoCheck(null);
|
||||
form.Show();
|
||||
|
||||
Assert.Single(nest.Plates);
|
||||
Assert.Equal("New plate (empty)", Header(form));
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void InteriorEmptyPlate_KeepsItsNumberAndTheTotalCountsIt() => RunSta(() =>
|
||||
{
|
||||
var nest = new Nest("interior empty");
|
||||
nest.CreatePlate().Parts.Add(Square("a"));
|
||||
nest.CreatePlate(); // An empty interior plate survives EnsureSentinel (only the tail trims).
|
||||
nest.CreatePlate().Parts.Add(Square("b"));
|
||||
using var form = new EditNestForm(nest);
|
||||
form.PlateView.SetOverlapAutoCheck(null);
|
||||
form.Show();
|
||||
|
||||
Assert.Equal(4, nest.Plates.Count); // three real + sentinel
|
||||
form.PlateManager.LoadAt(1);
|
||||
Assert.Equal("Plate 2 of 3", Header(form).Split(" | ")[0]);
|
||||
form.PlateManager.LoadAt(2);
|
||||
Assert.Equal("Plate 3 of 3", Header(form).Split(" | ")[0]);
|
||||
form.PlateManager.LoadAt(3);
|
||||
Assert.Equal("New plate (empty)", Header(form));
|
||||
Assert.Equal(4, nest.Plates.Count);
|
||||
});
|
||||
|
||||
private static string Header(EditNestForm form) =>
|
||||
((Label)typeof(EditNestForm)
|
||||
.GetField("plateInfoLabel", BindingFlags.Instance | BindingFlags.NonPublic)!
|
||||
.GetValue(form)!).Text;
|
||||
|
||||
private static Part Square(string name)
|
||||
{
|
||||
var program = new Program();
|
||||
program.MoveTo(0, 0);
|
||||
program.LineTo(0, 10);
|
||||
program.LineTo(10, 10);
|
||||
program.LineTo(10, 0);
|
||||
program.LineTo(0, 0);
|
||||
return new Part(new Drawing(name, program), new Vector(1, 1));
|
||||
}
|
||||
|
||||
private static void RunSta(System.Action action) =>
|
||||
StaTestThread.Run(action, TimeSpan.FromMinutes(3), "The STA test did not complete.");
|
||||
}
|
||||
@@ -85,10 +85,15 @@ public partial class CuttingPlanForm : Form
|
||||
Text = "Plan Cutting — All Plates";
|
||||
applyButton.Text = "&Apply to All Plates";
|
||||
}
|
||||
var activeNumber = all.IndexOf(activePlate) + 1;
|
||||
previewText = plates.Contains(activePlate)
|
||||
? $"Plate {activeNumber} of {all.Count}, numbered in cutting order:"
|
||||
: $"Plate {activeNumber} has no parts; plates with parts are listed on the right.";
|
||||
// Display-only numbering: the trailing empty new-plate sentinel is excluded from the
|
||||
// "of M" total; plateNumbers keep real collection positions for the batch summary.
|
||||
var displayCount = PlateDisplayNumbering.DisplayedPlateCount(all);
|
||||
var activeNumber = PlateDisplayNumbering.DisplayedPlateNumber(all, all.IndexOf(activePlate));
|
||||
previewText = activeNumber == null
|
||||
? "New plate (empty) has no parts; plates with parts are listed on the right."
|
||||
: plates.Contains(activePlate)
|
||||
? $"Plate {activeNumber} of {displayCount}, numbered in cutting order:"
|
||||
: $"Plate {activeNumber} has no parts; plates with parts are listed on the right.";
|
||||
previewLabel.Text = previewText;
|
||||
ShowSettings();
|
||||
}
|
||||
|
||||
@@ -781,19 +781,10 @@ namespace OpenNest.Forms
|
||||
{
|
||||
var plate = PlateManager.CurrentPlate;
|
||||
|
||||
if (plate != null)
|
||||
{
|
||||
plateInfoLabel.Text = string.Format(
|
||||
"Plate {0} of {1} | {2}",
|
||||
PlateManager.CurrentIndex + 1,
|
||||
PlateManager.Count,
|
||||
plate.Size
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
plateInfoLabel.Text = "No plates";
|
||||
}
|
||||
// Display-only numbering: the trailing empty new-plate sentinel is excluded from the
|
||||
// total (and labeled as such); navigation and storage indexes are untouched.
|
||||
plateInfoLabel.Text = PlateDisplayNumbering.FormatHeader(
|
||||
Nest.Plates, PlateManager.CurrentIndex, plate.Size.ToString());
|
||||
|
||||
btnFirstPlate.Enabled = !PlateManager.IsFirst;
|
||||
btnPreviousPlate.Enabled = !PlateManager.IsFirst;
|
||||
|
||||
Reference in new issue
Block a user