diff --git a/OpenNest.WinForms.Tests/Controls/PlateViewReorderTests.cs b/OpenNest.WinForms.Tests/Controls/PlateViewReorderTests.cs new file mode 100644 index 0000000..a44ad7a --- /dev/null +++ b/OpenNest.WinForms.Tests/Controls/PlateViewReorderTests.cs @@ -0,0 +1,42 @@ +using OpenNest.Controls; +using OpenNest.Geometry; + +namespace OpenNest.WinForms.Tests.Controls; + +public class PlateViewReorderTests +{ + [Fact] + public void FollowsAnInPlaceReorderOfItsCurrentPlateOnly() => StaTestThread.Run(() => + { + var plate = new Plate(100, 100); + foreach (var x in new[] { 1.0, 12.0, 23.0 }) + plate.Parts.Add(new Part(new Drawing($"part {x}", Square()), new Vector(x, 1))); + using var view = new PlateView { Plate = plate }; + var parts = plate.Parts.ToArray(); + var raised = 0; + view.PartsReordered += (_, _) => raised++; + + plate.Parts.Reorder(new[] { parts[2], parts[0], parts[1] }); + + // Part numbers are drawn from the layout order, so it must follow the plate. + Assert.Equal(1, raised); + Assert.Equal(new[] { parts[2], parts[0], parts[1] }, view.LayoutParts.Select(layout => layout.BasePart)); + Assert.All(view.LayoutParts, layout => Assert.True(layout.IsDirty)); + + view.Plate = new Plate(60, 120); + plate.Parts.Reorder(parts); + Assert.Equal(1, raised); + Assert.Empty(view.LayoutParts); + }, TimeSpan.FromMinutes(1), "The STA test did not complete."); + + private static CNC.Program Square() + { + var program = new CNC.Program(); + program.MoveTo(0, 0); + program.LineTo(0, 10); + program.LineTo(10, 10); + program.LineTo(10, 0); + program.LineTo(0, 0); + return program; + } +} diff --git a/OpenNest/Controls/OverlapOverlayController.cs b/OpenNest/Controls/OverlapOverlayController.cs index 6e1ce3d..96e71e6 100644 --- a/OpenNest/Controls/OverlapOverlayController.cs +++ b/OpenNest/Controls/OverlapOverlayController.cs @@ -116,6 +116,7 @@ internal sealed class OverlapOverlayController : IDisposable observedParts.ItemAdded += PartAdded; observedParts.ItemRemoved += PartRemoved; observedParts.ItemChanged += PartChanged; + observedParts.ItemsReordered += PartsReordered; } NotifyChanged(); } @@ -333,6 +334,7 @@ internal sealed class OverlapOverlayController : IDisposable private void PartAdded(object sender, ItemAddedEventArgs e) => Invalidate(); private void PartRemoved(object sender, ItemRemovedEventArgs e) => Invalidate(); private void PartChanged(object sender, ItemChangedEventArgs e) => Invalidate(); + private void PartsReordered(object sender, EventArgs e) => Invalidate(); private void Unsubscribe() { @@ -341,6 +343,7 @@ internal sealed class OverlapOverlayController : IDisposable observedParts.ItemAdded -= PartAdded; observedParts.ItemRemoved -= PartRemoved; observedParts.ItemChanged -= PartChanged; + observedParts.ItemsReordered -= PartsReordered; observedParts = null; } diff --git a/OpenNest/Controls/PlateView.cs b/OpenNest/Controls/PlateView.cs index c3c01ef..bf4a695 100644 --- a/OpenNest/Controls/PlateView.cs +++ b/OpenNest/Controls/PlateView.cs @@ -76,6 +76,12 @@ namespace OpenNest.Controls public event EventHandler> PartAdded; public event EventHandler> PartRemoved; + + /// + /// Raised after the plate's parts were reordered in place, for example by an applied cutting + /// plan that also installed new programs. The view has already redrawn its parts in the new order. + /// + public event EventHandler PartsReordered; public event EventHandler StatusChanged; public event EventHandler SelectionChanged @@ -200,6 +206,7 @@ namespace OpenNest.Controls { plate.PartAdded -= plate_PartAdded; plate.PartRemoved -= plate_PartRemoved; + plate.PartsReordered -= plate_PartsReordered; parts.Clear(); previewManager.Clear(); selection.Clear(); @@ -208,6 +215,7 @@ namespace OpenNest.Controls plate = p; plate.PartAdded += plate_PartAdded; plate.PartRemoved += plate_PartRemoved; + plate.PartsReordered += plate_PartsReordered; foreach (var part in plate.Parts) parts.Add(LayoutPart.Create(part, this)); @@ -818,6 +826,25 @@ namespace OpenNest.Controls parts.RemoveAll(p => p.BasePart == e.Item); } + private void plate_PartsReordered(object sender, EventArgs e) + { + // Part numbers are drawn from the layout order, and an applied cutting plan can also + // replace programs, so follow the plate's order and rebuild every part's graphics. + var layouts = new Dictionary(ReferenceEqualityComparer.Instance); + foreach (var layout in parts) + layouts.TryAdd(layout.BasePart, layout); + var reordered = plate.Parts + .Select(part => layouts.TryGetValue(part, out var layout) ? layout : LayoutPart.Create(part, this)) + .ToList(); + parts.Clear(); + parts.AddRange(reordered); + foreach (var layout in parts) + layout.IsDirty = true; + + PartsReordered?.Invoke(this, EventArgs.Empty); + Invalidate(); + } + public void DeselectAll() => selection.DeselectAll(); public void SelectAll() => selection.SelectAll();