From a0ac7e5756cb82381d15e4624cbd307fe7b7126e Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Mon, 5 Oct 2026 21:21:38 -0400 Subject: [PATCH] feat(ui): redraw the plate view when its plate is reordered PlateView now follows Plate.PartsReordered, which an applied cutting plan raises once per changed plate: it puts its layout parts in the plate's order (the drawn part numbers are the cutting order), marks them dirty so new programs are redrawn, and raises its own PartsReordered. The overlap overlay treats a reorder like any other part change and marks its report out of date. --- .../Controls/PlateViewReorderTests.cs | 42 +++++++++++++++++++ OpenNest/Controls/OverlapOverlayController.cs | 3 ++ OpenNest/Controls/PlateView.cs | 27 ++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 OpenNest.WinForms.Tests/Controls/PlateViewReorderTests.cs 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();