From ed082a6799bacd41418fb005a202c6690aea1bb8 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Sun, 5 Apr 2026 23:47:18 -0400 Subject: [PATCH] feat: add PlateManager with navigation state and disposal Introduces PlateChangedEventArgs and PlateManager in OpenNest.Core to centralize plate navigation logic (CurrentIndex, LoadFirst/Last/Next/Previous/At, IsFirst/IsLast). Includes full xUnit test coverage (17 tests) verifying navigation, event firing, and disposal unsubscription. Co-Authored-By: Claude Sonnet 4.6 --- OpenNest.Core/PlateManager.cs | 118 ++++++++++++++++ OpenNest.Tests/PlateManagerTests.cs | 208 ++++++++++++++++++++++++++++ 2 files changed, 326 insertions(+) create mode 100644 OpenNest.Core/PlateManager.cs create mode 100644 OpenNest.Tests/PlateManagerTests.cs diff --git a/OpenNest.Core/PlateManager.cs b/OpenNest.Core/PlateManager.cs new file mode 100644 index 0000000..f7618dc --- /dev/null +++ b/OpenNest.Core/PlateManager.cs @@ -0,0 +1,118 @@ +using OpenNest.Collections; +using System; + +namespace OpenNest +{ + public class PlateChangedEventArgs : EventArgs + { + public Plate Plate { get; } + public int Index { get; } + + public PlateChangedEventArgs(Plate plate, int index) + { + Plate = plate; + Index = index; + } + } + + public class PlateManager : IDisposable + { + private readonly Nest nest; + private bool disposed; + + public event EventHandler CurrentPlateChanged; + public event EventHandler PlateListChanged; + + public PlateManager(Nest nest) + { + this.nest = nest; + nest.Plates.ItemAdded += OnPlateAdded; + nest.Plates.ItemRemoved += OnPlateRemoved; + } + + public int CurrentIndex { get; private set; } + + public Plate CurrentPlate => nest.Plates.Count > 0 ? nest.Plates[CurrentIndex] : null; + + public int Count => nest.Plates.Count; + + public bool IsFirst => Count == 0 || CurrentIndex <= 0; + + public bool IsLast => CurrentIndex + 1 >= Count; + + public void LoadFirst() + { + if (Count == 0) + return; + + CurrentIndex = 0; + FireCurrentPlateChanged(); + } + + public void LoadLast() + { + if (Count == 0) + return; + + CurrentIndex = Count - 1; + FireCurrentPlateChanged(); + } + + public bool LoadNext() + { + if (CurrentIndex + 1 >= Count) + return false; + + CurrentIndex++; + FireCurrentPlateChanged(); + return true; + } + + public bool LoadPrevious() + { + if (Count == 0 || CurrentIndex - 1 < 0) + return false; + + CurrentIndex--; + FireCurrentPlateChanged(); + return true; + } + + public void LoadAt(int index) + { + if (index < 0 || index >= Count) + return; + + CurrentIndex = index; + FireCurrentPlateChanged(); + } + + private void OnPlateAdded(object sender, ItemAddedEventArgs e) + { + PlateListChanged?.Invoke(this, EventArgs.Empty); + } + + private void OnPlateRemoved(object sender, ItemRemovedEventArgs e) + { + if (CurrentIndex >= Count && Count > 0) + CurrentIndex = Count - 1; + + PlateListChanged?.Invoke(this, EventArgs.Empty); + } + + private void FireCurrentPlateChanged() + { + CurrentPlateChanged?.Invoke(this, new PlateChangedEventArgs(CurrentPlate, CurrentIndex)); + } + + public void Dispose() + { + if (disposed) + return; + + disposed = true; + nest.Plates.ItemAdded -= OnPlateAdded; + nest.Plates.ItemRemoved -= OnPlateRemoved; + } + } +} diff --git a/OpenNest.Tests/PlateManagerTests.cs b/OpenNest.Tests/PlateManagerTests.cs new file mode 100644 index 0000000..787a675 --- /dev/null +++ b/OpenNest.Tests/PlateManagerTests.cs @@ -0,0 +1,208 @@ +using OpenNest.CNC; +using OpenNest.Geometry; + +namespace OpenNest.Tests; + +public class PlateManagerTests +{ + private static Nest CreateNest() + { + var nest = new Nest("test"); + return nest; + } + + 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))); + var drawing = new Drawing("test", pgm); + return new Part(drawing); + } + + [Fact] + public void Constructor_EmptyNest_CurrentIndexZero() + { + var nest = CreateNest(); + using var mgr = new PlateManager(nest); + Assert.Equal(0, mgr.CurrentIndex); + } + + [Fact] + public void Constructor_NestWithPlates_CurrentIndexZero() + { + var nest = CreateNest(); + nest.CreatePlate(); + nest.CreatePlate(); + using var mgr = new PlateManager(nest); + Assert.Equal(0, mgr.CurrentIndex); + } + + [Fact] + public void CurrentPlate_ReturnsPlateAtCurrentIndex() + { + var nest = CreateNest(); + var plate = nest.CreatePlate(); + using var mgr = new PlateManager(nest); + Assert.Same(plate, mgr.CurrentPlate); + } + + [Fact] + public void CurrentPlate_EmptyNest_ReturnsNull() + { + var nest = CreateNest(); + using var mgr = new PlateManager(nest); + Assert.Null(mgr.CurrentPlate); + } + + [Fact] + public void Count_DelegatesToNestPlates() + { + var nest = CreateNest(); + nest.CreatePlate(); + nest.CreatePlate(); + using var mgr = new PlateManager(nest); + Assert.Equal(2, mgr.Count); + } + + [Fact] + public void LoadFirst_SetsCurrentIndexToZero() + { + var nest = CreateNest(); + nest.CreatePlate(); + nest.CreatePlate(); + using var mgr = new PlateManager(nest); + mgr.LoadLast(); + mgr.LoadFirst(); + Assert.Equal(0, mgr.CurrentIndex); + } + + [Fact] + public void LoadLast_SetsCurrentIndexToLastPlate() + { + var nest = CreateNest(); + nest.CreatePlate(); + nest.CreatePlate(); + nest.CreatePlate(); + using var mgr = new PlateManager(nest); + mgr.LoadLast(); + Assert.Equal(2, mgr.CurrentIndex); + } + + [Fact] + public void LoadNext_AdvancesIndex() + { + var nest = CreateNest(); + nest.CreatePlate(); + nest.CreatePlate(); + using var mgr = new PlateManager(nest); + var result = mgr.LoadNext(); + Assert.True(result); + Assert.Equal(1, mgr.CurrentIndex); + } + + [Fact] + public void LoadNext_AtEnd_ReturnsFalse() + { + var nest = CreateNest(); + nest.CreatePlate(); + using var mgr = new PlateManager(nest); + var result = mgr.LoadNext(); + Assert.False(result); + Assert.Equal(0, mgr.CurrentIndex); + } + + [Fact] + public void LoadPrevious_DecrementsIndex() + { + var nest = CreateNest(); + nest.CreatePlate(); + nest.CreatePlate(); + using var mgr = new PlateManager(nest); + mgr.LoadLast(); + var result = mgr.LoadPrevious(); + Assert.True(result); + Assert.Equal(0, mgr.CurrentIndex); + } + + [Fact] + public void LoadPrevious_AtStart_ReturnsFalse() + { + var nest = CreateNest(); + nest.CreatePlate(); + using var mgr = new PlateManager(nest); + var result = mgr.LoadPrevious(); + Assert.False(result); + Assert.Equal(0, mgr.CurrentIndex); + } + + [Fact] + public void LoadAt_SetsExactIndex() + { + var nest = CreateNest(); + nest.CreatePlate(); + nest.CreatePlate(); + nest.CreatePlate(); + using var mgr = new PlateManager(nest); + mgr.LoadAt(2); + Assert.Equal(2, mgr.CurrentIndex); + } + + [Fact] + public void IsFirst_WhenAtStart_ReturnsTrue() + { + var nest = CreateNest(); + nest.CreatePlate(); + using var mgr = new PlateManager(nest); + Assert.True(mgr.IsFirst); + } + + [Fact] + public void IsLast_WhenAtEnd_ReturnsTrue() + { + var nest = CreateNest(); + nest.CreatePlate(); + using var mgr = new PlateManager(nest); + Assert.True(mgr.IsLast); + } + + [Fact] + public void IsFirst_WhenNotAtStart_ReturnsFalse() + { + var nest = CreateNest(); + nest.CreatePlate(); + nest.CreatePlate(); + using var mgr = new PlateManager(nest); + mgr.LoadLast(); + Assert.False(mgr.IsFirst); + } + + [Fact] + public void Navigation_FiresCurrentPlateChanged() + { + var nest = CreateNest(); + nest.CreatePlate(); + nest.CreatePlate(); + using var mgr = new PlateManager(nest); + PlateChangedEventArgs received = null; + mgr.CurrentPlateChanged += (s, e) => received = e; + mgr.LoadNext(); + Assert.NotNull(received); + Assert.Equal(1, received.Index); + Assert.Same(nest.Plates[1], received.Plate); + } + + [Fact] + public void Dispose_UnsubscribesFromPlateEvents() + { + var nest = CreateNest(); + using var mgr = new PlateManager(nest); + var eventFired = false; + mgr.PlateListChanged += (s, e) => eventFired = true; + mgr.Dispose(); + nest.CreatePlate(); + Assert.False(eventFired); + } +}