From faf3c9bcc496eedd9a015a92ddf2bf231e2b2e24 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Mon, 5 Oct 2026 21:39:58 -0400 Subject: [PATCH] fix(ui): detach disposed plate views and keep repeated layout parts A disposed PlateView stayed subscribed to its plate, so a plate that outlived the view kept rebuilding the disposed view's layout and raising its events; Dispose now detaches the part added, removed and reordered handlers. ObservableList.Reorder keeps repeated references, but the reorder handler mapped each part to one layout, so [a, a, b] collapsed to one shared layout for both occurrences of a. It now reuses each existing layout once and creates one only when none is left. --- .../Controls/PlateViewReorderTests.cs | 41 +++++++++++++++++++ OpenNest/Controls/PlateView.cs | 20 +++++++-- 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/OpenNest.WinForms.Tests/Controls/PlateViewReorderTests.cs b/OpenNest.WinForms.Tests/Controls/PlateViewReorderTests.cs index a44ad7a..4e14632 100644 --- a/OpenNest.WinForms.Tests/Controls/PlateViewReorderTests.cs +++ b/OpenNest.WinForms.Tests/Controls/PlateViewReorderTests.cs @@ -29,6 +29,47 @@ public class PlateViewReorderTests Assert.Empty(view.LayoutParts); }, TimeSpan.FromMinutes(1), "The STA test did not complete."); + [Fact] + public void KeepsOneLayoutPerOccurrenceOfARepeatedPart() => StaTestThread.Run(() => + { + var plate = new Plate(100, 100); + var repeated = new Part(new Drawing("repeated", Square()), new Vector(1, 1)); + var other = new Part(new Drawing("other", Square()), new Vector(12, 1)); + plate.Parts.Add(repeated); + plate.Parts.Add(repeated); + plate.Parts.Add(other); + using var view = new PlateView { Plate = plate }; + var before = view.LayoutParts.ToArray(); + + plate.Parts.Reorder(new[] { other, repeated, repeated }); + + var after = view.LayoutParts; + Assert.Equal(new[] { other, repeated, repeated }, after.Select(layout => layout.BasePart)); + Assert.Same(before[2], after[0]); + Assert.Equal(3, after.Distinct().Count()); + Assert.Equal(before.ToHashSet(), after.ToHashSet()); + }, TimeSpan.FromMinutes(1), "The STA test did not complete."); + + [Fact] + public void DisposedViewIgnoresItsRetainedPlate() => StaTestThread.Run(() => + { + var plate = new Plate(100, 100); + foreach (var x in new[] { 1.0, 12.0 }) + plate.Parts.Add(new Part(new Drawing($"part {x}", Square()), new Vector(x, 1))); + var parts = plate.Parts.ToArray(); + var view = new PlateView { Plate = plate }; + var raised = 0; + view.PartsReordered += (_, _) => raised++; + var layouts = view.LayoutParts.ToArray(); + + view.Dispose(); + plate.Parts.Reorder(new[] { parts[1], parts[0] }); + plate.Parts.Add(new Part(new Drawing("late", Square()), new Vector(23, 1))); + + Assert.Equal(0, raised); + Assert.Equal(layouts, view.LayoutParts); + }, TimeSpan.FromMinutes(1), "The STA test did not complete."); + private static CNC.Program Square() { var program = new CNC.Program(); diff --git a/OpenNest/Controls/PlateView.cs b/OpenNest/Controls/PlateView.cs index bf4a695..47f15b6 100644 --- a/OpenNest/Controls/PlateView.cs +++ b/OpenNest/Controls/PlateView.cs @@ -627,6 +627,13 @@ namespace OpenNest.Controls { if (disposing) { + // A plate can outlive its view; stop it calling back into a disposed one. + if (plate != null) + { + plate.PartAdded -= plate_PartAdded; + plate.PartRemoved -= plate_PartRemoved; + plate.PartsReordered -= plate_PartsReordered; + } overlapOverlay?.Dispose(); hoverTimer?.Dispose(); hoverTimer = null; @@ -830,11 +837,18 @@ namespace OpenNest.Controls { // 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); + // A reorder keeps repeated references, so reuse each existing layout once. + var layouts = new Dictionary>(ReferenceEqualityComparer.Instance); foreach (var layout in parts) - layouts.TryAdd(layout.BasePart, layout); + { + if (!layouts.TryGetValue(layout.BasePart, out var queue)) + layouts.Add(layout.BasePart, queue = new Queue()); + queue.Enqueue(layout); + } var reordered = plate.Parts - .Select(part => layouts.TryGetValue(part, out var layout) ? layout : LayoutPart.Create(part, this)) + .Select(part => layouts.TryGetValue(part, out var queue) && queue.Count > 0 + ? queue.Dequeue() + : LayoutPart.Create(part, this)) .ToList(); parts.Clear(); parts.AddRange(reordered);