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.
This commit is contained in:
aj committed 2026-10-05 21:21:38 -04:00
1 parent 9a4f73b924
commit a0ac7e5756
3 files changed
+72

No files matched your search

@@ -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;
}
}
@@ -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<Part> e) => Invalidate();
private void PartRemoved(object sender, ItemRemovedEventArgs<Part> e) => Invalidate();
private void PartChanged(object sender, ItemChangedEventArgs<Part> 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;
}
+27
View File
@@ -76,6 +76,12 @@ namespace OpenNest.Controls
public event EventHandler<ItemAddedEventArgs<Part>> PartAdded;
public event EventHandler<ItemRemovedEventArgs<Part>> PartRemoved;
/// <summary>
/// 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.
/// </summary>
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<Part, LayoutPart>(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();