# Nesting Progress Window Implementation Plan > **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Add a live-preview progress window so users can watch nesting results appear on the plate and stop early if satisfied. **Architecture:** The engine gets new `List`-returning overloads that accept `IProgress` + `CancellationToken`. A modeless `NestProgressForm` shows stats. `PlateView` gains a temporary parts list drawn in a preview color. `MainForm` orchestrates everything via `Task.Run` + `await`. **Tech Stack:** .NET 8 WinForms, `IProgress`, `CancellationToken`, `Task.Run` **Spec:** `docs/superpowers/specs/2026-03-13-nesting-progress-window-design.md` --- ## File Structure | File | Responsibility | |------|---------------| | `OpenNest.Engine/NestProgress.cs` | **New.** `NestPhase` enum + `NestProgress` class (progress data model) | | `OpenNest.Engine/NestEngine.cs` | **Modify.** Add `List`-returning overloads with progress/cancellation. Existing `bool` overloads delegate to them. | | `OpenNest/ColorScheme.cs` | **Modify.** Add `PreviewPartColor` + `PreviewPartPen` + `PreviewPartBrush` | | `OpenNest/Controls/PlateView.cs` | **Modify.** Add `temporaryParts` list, draw them in preview color, expose `SetTemporaryParts`/`ClearTemporaryParts`/`AcceptTemporaryParts` | | `OpenNest/Forms/NestProgressForm.cs` | **New.** Modeless dialog with TableLayoutPanel showing stats + Stop button | | `OpenNest/Forms/MainForm.cs` | **Modify.** Rewire `RunAutoNest_Click` and `FillPlate_Click` to async with progress. Add UI lockout. | | `OpenNest/Actions/ActionFillArea.cs` | **Modify.** Use progress overload for area fill. | --- ## Chunk 1: Engine Progress Infrastructure ### Task 1: NestProgress Data Model **Files:** - Create: `OpenNest.Engine/NestProgress.cs` - [ ] **Step 1: Create NestPhase enum and NestProgress class** ```csharp // OpenNest.Engine/NestProgress.cs using System.Collections.Generic; namespace OpenNest { public enum NestPhase { Linear, RectBestFit, Pairs, Remainder } public class NestProgress { public NestPhase Phase { get; set; } public int PlateNumber { get; set; } public int BestPartCount { get; set; } public double BestDensity { get; set; } public double UsableRemnantArea { get; set; } public List BestParts { get; set; } } } ``` - [ ] **Step 2: Build to verify** Run: `dotnet build OpenNest.Engine` Expected: Build succeeded - [ ] **Step 3: Commit** ```bash git add OpenNest.Engine/NestProgress.cs git commit -m "feat(engine): add NestPhase enum and NestProgress data model" ``` --- ### Task 2: NestEngine Progress Overloads — FindBestFill This task adds the core progress/cancellation support to `FindBestFill`, the private method that tries Linear → RectBestFit → Pairs → Remainder strategies. The new method signature adds `IProgress` and `CancellationToken` parameters. **Files:** - Modify: `OpenNest.Engine/NestEngine.cs` **Context:** `FindBestFill` is currently at line 55. It uses `Parallel.ForEach` for the linear phase (line 90) and `FillWithPairs` uses `Parallel.For` (line 224). Both need the cancellation token threaded into `ParallelOptions`. - [ ] **Step 1: Add a private helper to report progress** Add this helper method at the end of `NestEngine` (before the closing brace at line 536). It clones the best parts list and reports via `IProgress`: ```csharp private static void ReportProgress( IProgress progress, NestPhase phase, int plateNumber, List best, Box workArea) { if (progress == null || best == null || best.Count == 0) return; var score = FillScore.Compute(best, workArea); var clonedParts = new List(best.Count); foreach (var part in best) clonedParts.Add((Part)part.Clone()); progress.Report(new NestProgress { Phase = phase, PlateNumber = plateNumber, BestPartCount = score.Count, BestDensity = score.Density, UsableRemnantArea = score.UsableRemnantArea, BestParts = clonedParts }); } ``` - [ ] **Step 2: Add a `PlateNumber` property to NestEngine** Add a public property so callers can set the current plate number for progress reporting. Add after the `NestDirection` property (line 20): ```csharp public int PlateNumber { get; set; } ``` - [ ] **Step 3: Create `FindBestFill` overload with progress and cancellation** Add a new private method below the existing `FindBestFill` (after line 135). This is a copy of `FindBestFill` with progress reporting and cancellation token support injected at each phase boundary. Key changes: - `Parallel.ForEach` gets `new ParallelOptions { CancellationToken = token }` - After each phase (linear, rect, pairs), call `ReportProgress(...)` if a new best is found - After each phase, call `token.ThrowIfCancellationRequested()` - The whole method is wrapped in try/catch for `OperationCanceledException` to return current best ```csharp private List FindBestFill(NestItem item, Box workArea, IProgress progress, CancellationToken token) { List best = null; try { var bestRotation = RotationAnalysis.FindBestRotation(item); var engine = new FillLinear(workArea, Plate.PartSpacing); var angles = new List { bestRotation, bestRotation + Angle.HalfPI }; var testPart = new Part(item.Drawing); if (!bestRotation.IsEqualTo(0)) testPart.Rotate(bestRotation); testPart.UpdateBounds(); var partLongestSide = System.Math.Max(testPart.BoundingBox.Width, testPart.BoundingBox.Length); var workAreaShortSide = System.Math.Min(workArea.Width, workArea.Length); if (workAreaShortSide < partLongestSide) { var step = Angle.ToRadians(5); for (var a = 0.0; a < System.Math.PI; a += step) { if (!angles.Any(existing => existing.IsEqualTo(a))) angles.Add(a); } } // Linear phase var linearBag = new System.Collections.Concurrent.ConcurrentBag<(FillScore score, List parts)>(); System.Threading.Tasks.Parallel.ForEach(angles, new System.Threading.Tasks.ParallelOptions { CancellationToken = token }, angle => { var localEngine = new FillLinear(workArea, Plate.PartSpacing); var h = localEngine.Fill(item.Drawing, angle, NestDirection.Horizontal); var v = localEngine.Fill(item.Drawing, angle, NestDirection.Vertical); if (h != null && h.Count > 0) linearBag.Add((FillScore.Compute(h, workArea), h)); if (v != null && v.Count > 0) linearBag.Add((FillScore.Compute(v, workArea), v)); }); var bestScore = default(FillScore); foreach (var (score, parts) in linearBag) { if (best == null || score > bestScore) { best = parts; bestScore = score; } } var bestLinearScore = best != null ? FillScore.Compute(best, workArea) : default; Debug.WriteLine($"[FindBestFill] Linear: {bestLinearScore.Count} parts, density={bestLinearScore.Density:P1} | WorkArea: {workArea.Width:F1}x{workArea.Length:F1} | Angles: {angles.Count}"); ReportProgress(progress, NestPhase.Linear, PlateNumber, best, workArea); token.ThrowIfCancellationRequested(); // RectBestFit phase var rectResult = FillRectangleBestFit(item, workArea); Debug.WriteLine($"[FindBestFill] RectBestFit: {rectResult?.Count ?? 0} parts"); if (IsBetterFill(rectResult, best, workArea)) { best = rectResult; ReportProgress(progress, NestPhase.RectBestFit, PlateNumber, best, workArea); } token.ThrowIfCancellationRequested(); // Pairs phase var pairResult = FillWithPairs(item, workArea, token); Debug.WriteLine($"[FindBestFill] Pair: {pairResult.Count} parts | Winner: {(IsBetterFill(pairResult, best, workArea) ? "Pair" : "Linear")}"); if (IsBetterFill(pairResult, best, workArea)) { best = pairResult; ReportProgress(progress, NestPhase.Pairs, PlateNumber, best, workArea); } } catch (OperationCanceledException) { Debug.WriteLine("[FindBestFill] Cancelled, returning current best"); } return best ?? new List(); } ``` - [ ] **Step 4: Build to verify** Run: `dotnet build OpenNest.Engine` Expected: Build succeeded (the new overload exists but isn't called yet) - [ ] **Step 5: Commit** ```bash git add OpenNest.Engine/NestEngine.cs git commit -m "feat(engine): add FindBestFill overload with progress and cancellation" ``` --- ### Task 3: NestEngine Progress Overloads — FillWithPairs Thread the `CancellationToken` into `FillWithPairs` so its `Parallel.For` loop can be cancelled mid-phase. **Files:** - Modify: `OpenNest.Engine/NestEngine.cs` **Context:** `FillWithPairs` is at line 213. It uses `Parallel.For` at line 224. - [ ] **Step 1: Add FillWithPairs overload accepting CancellationToken** Add a new private method below the existing `FillWithPairs` (after line 250). This is a copy with `ParallelOptions` added to the `Parallel.For` call: ```csharp private List FillWithPairs(NestItem item, Box workArea, CancellationToken token) { var bestFits = BestFitCache.GetOrCompute( item.Drawing, Plate.Size.Width, Plate.Size.Length, Plate.PartSpacing); var candidates = SelectPairCandidates(bestFits, workArea); Debug.WriteLine($"[FillWithPairs] Total: {bestFits.Count}, Kept: {bestFits.Count(r => r.Keep)}, Trying: {candidates.Count}"); var resultBag = new System.Collections.Concurrent.ConcurrentBag<(FillScore score, List parts)>(); try { System.Threading.Tasks.Parallel.For(0, candidates.Count, new System.Threading.Tasks.ParallelOptions { CancellationToken = token }, i => { var result = candidates[i]; var pairParts = result.BuildParts(item.Drawing); var angles = RotationAnalysis.FindHullEdgeAngles(pairParts); var engine = new FillLinear(workArea, Plate.PartSpacing); var filled = FillPattern(engine, pairParts, angles, workArea); if (filled != null && filled.Count > 0) resultBag.Add((FillScore.Compute(filled, workArea), filled)); }); } catch (OperationCanceledException) { Debug.WriteLine("[FillWithPairs] Cancelled mid-phase, using results so far"); } List best = null; var bestScore = default(FillScore); foreach (var (score, parts) in resultBag) { if (best == null || score > bestScore) { best = parts; bestScore = score; } } Debug.WriteLine($"[FillWithPairs] Best pair result: {bestScore.Count} parts, remnant={bestScore.UsableRemnantArea:F1}, density={bestScore.Density:P1}"); return best ?? new List(); } ``` - [ ] **Step 2: Build to verify** Run: `dotnet build OpenNest.Engine` Expected: Build succeeded - [ ] **Step 3: Commit** ```bash git add OpenNest.Engine/NestEngine.cs git commit -m "feat(engine): add FillWithPairs overload with CancellationToken" ``` --- ### Task 4: NestEngine Public Fill Overloads Add the public `List`-returning `Fill` overloads that callers (MainForm) will use. These call `FindBestFill` with progress/cancellation and return the result without adding to `Plate.Parts`. Wire the existing `bool Fill(...)` overloads to delegate to them. **Files:** - Modify: `OpenNest.Engine/NestEngine.cs` **Context:** The existing `bool Fill(NestItem item, Box workArea)` is at line 32. The existing `bool Fill(List groupParts, Box workArea)` is at line 137. - [ ] **Step 1: Add public Fill(NestItem, Box, IProgress, CancellationToken) overload** Add after the existing `Fill(NestItem, Box)` method (after line 53). This calls `FindBestFill` with progress/cancellation, applies `TryRemainderImprovement`, and returns the parts without adding to the plate: ```csharp public List Fill(NestItem item, Box workArea, IProgress progress, CancellationToken token) { var best = FindBestFill(item, workArea, progress, token); if (token.IsCancellationRequested) return best ?? new List(); // Try improving by filling the remainder strip separately. var improved = TryRemainderImprovement(item, workArea, best); if (IsBetterFill(improved, best, workArea)) { Debug.WriteLine($"[Fill] Remainder improvement: {improved.Count} parts (was {best?.Count ?? 0})"); best = improved; ReportProgress(progress, NestPhase.Remainder, PlateNumber, best, workArea); } if (best == null || best.Count == 0) return new List(); if (item.Quantity > 0 && best.Count > item.Quantity) best = best.Take(item.Quantity).ToList(); return best; } ``` - [ ] **Step 2: Rewire existing bool Fill(NestItem, Box) to delegate** Replace the body of `Fill(NestItem item, Box workArea)` at line 32 to delegate to the new overload: ```csharp public bool Fill(NestItem item, Box workArea) { var parts = Fill(item, workArea, null, CancellationToken.None); if (parts == null || parts.Count == 0) return false; Plate.Parts.AddRange(parts); return true; } ``` Add `using System.Threading;` to the top of the file if not already present. - [ ] **Step 3: Add public Fill(List\, Box, IProgress, CancellationToken) overload** Add after the existing `Fill(List groupParts, Box workArea)` method (after line 180). Same pattern — compute and return without touching `Plate.Parts`: ```csharp public List Fill(List groupParts, Box workArea, IProgress progress, CancellationToken token) { if (groupParts == null || groupParts.Count == 0) return new List(); var engine = new FillLinear(workArea, Plate.PartSpacing); var angles = RotationAnalysis.FindHullEdgeAngles(groupParts); var best = FillPattern(engine, groupParts, angles, workArea); Debug.WriteLine($"[Fill(groupParts,Box)] Linear: {best?.Count ?? 0} parts | WorkArea: {workArea.Width:F1}x{workArea.Length:F1}"); ReportProgress(progress, NestPhase.Linear, PlateNumber, best, workArea); if (groupParts.Count == 1) { try { token.ThrowIfCancellationRequested(); var nestItem = new NestItem { Drawing = groupParts[0].BaseDrawing }; var rectResult = FillRectangleBestFit(nestItem, workArea); Debug.WriteLine($"[Fill(groupParts,Box)] RectBestFit: {rectResult?.Count ?? 0} parts"); if (IsBetterFill(rectResult, best, workArea)) { best = rectResult; ReportProgress(progress, NestPhase.RectBestFit, PlateNumber, best, workArea); } token.ThrowIfCancellationRequested(); var pairResult = FillWithPairs(nestItem, workArea, token); Debug.WriteLine($"[Fill(groupParts,Box)] Pair: {pairResult.Count} parts | Winner: {(IsBetterFill(pairResult, best, workArea) ? "Pair" : "Linear")}"); if (IsBetterFill(pairResult, best, workArea)) { best = pairResult; ReportProgress(progress, NestPhase.Pairs, PlateNumber, best, workArea); } // Try improving by filling the remainder strip separately. var improved = TryRemainderImprovement(nestItem, workArea, best); if (IsBetterFill(improved, best, workArea)) { Debug.WriteLine($"[Fill(groupParts,Box)] Remainder improvement: {improved.Count} parts (was {best?.Count ?? 0})"); best = improved; ReportProgress(progress, NestPhase.Remainder, PlateNumber, best, workArea); } } catch (OperationCanceledException) { Debug.WriteLine("[Fill(groupParts,Box)] Cancelled, returning current best"); } } return best ?? new List(); } ``` - [ ] **Step 4: Rewire existing bool Fill(List\, Box) to delegate** Replace the body of `Fill(List groupParts, Box workArea)` at line 137: ```csharp public bool Fill(List groupParts, Box workArea) { var parts = Fill(groupParts, workArea, null, CancellationToken.None); if (parts == null || parts.Count == 0) return false; Plate.Parts.AddRange(parts); return true; } ``` - [ ] **Step 5: Build to verify** Run: `dotnet build OpenNest.sln` Expected: Build succeeded (full solution — ensures MCP and UI still compile) - [ ] **Step 6: Commit** ```bash git add OpenNest.Engine/NestEngine.cs git commit -m "feat(engine): add public Fill overloads returning List with progress/cancellation" ``` --- ## Chunk 2: PlateView Temporary Parts ### Task 5: Add PreviewPart Color to ColorScheme **Files:** - Modify: `OpenNest/ColorScheme.cs` **Context:** Colors follow a pattern: private backing field, public property with getter/setter that lazily creates Pen/Brush. See `LayoutOutlineColor` at line 46 for the pattern. The `Default` static instance is at line 15. - [ ] **Step 1: Add PreviewPart color fields and properties** Add a private field after the existing color fields (after line 13): ```csharp private Color previewPartColor; ``` Add a private `Pen` and `Brush` field alongside the existing ones. Find where `layoutOutlinePen` etc. are declared and add: ```csharp private Pen previewPartPen; private Brush previewPartBrush; ``` Add the `PreviewPartColor` property after the last color property (after `EdgeSpacingColor`, around line 136). Follow the same pattern as `LayoutOutlineColor`: ```csharp public Color PreviewPartColor { get { return previewPartColor; } set { previewPartColor = value; if (previewPartPen != null) previewPartPen.Dispose(); if (previewPartBrush != null) previewPartBrush.Dispose(); previewPartPen = new Pen(value, 1); previewPartBrush = new SolidBrush(Color.FromArgb(60, value)); } } public Pen PreviewPartPen => previewPartPen; public Brush PreviewPartBrush => previewPartBrush; ``` Note: The brush uses `Color.FromArgb(60, value)` for semi-transparency so parts look like a preview overlay, not solid placed parts. - [ ] **Step 2: Set default preview color** In the `Default` static property initializer (around line 15), add: ```csharp PreviewPartColor = Color.FromArgb(255, 140, 0), // orange ``` - [ ] **Step 3: Build to verify** Run: `dotnet build OpenNest` Expected: Build succeeded - [ ] **Step 4: Commit** ```bash git add OpenNest/ColorScheme.cs git commit -m "feat(ui): add PreviewPart color to ColorScheme" ``` --- ### Task 6: PlateView Temporary Parts List and Drawing **Files:** - Modify: `OpenNest/Controls/PlateView.cs` **Context:** - `parts` field (private `List`) at line ~27 - `DrawParts` method at line 453 - `LayoutPart.Create(Part, PlateView)` is the factory for wrapping Part → LayoutPart - `Refresh()` at line 376 calls `parts.ForEach(p => p.Update(this))` then `Invalidate()` - `SetPlate` at line 116 clears `parts` when switching plates - [ ] **Step 1: Add temporaryParts field** Add after the `parts` field declaration (around line 27): ```csharp private List temporaryParts = new List(); ``` - [ ] **Step 2: Draw temporary parts in DrawParts** In `DrawParts` (line 453), after the main part drawing loop (after line 470 where `part.Draw(g, ...)` is called for regular parts), add temporary part drawing before the `DrawOffset`/`DrawBounds`/`DrawRapid` section: ```csharp // Draw temporary (preview) parts for (var i = 0; i < temporaryParts.Count; i++) { var temp = temporaryParts[i]; if (temp.IsDirty) temp.Update(this); var path = temp.Path; var pathBounds = path.GetBounds(); if (!pathBounds.IntersectsWith(viewBounds)) continue; g.FillPath(ColorScheme.PreviewPartBrush, path); g.DrawPath(ColorScheme.PreviewPartPen, path); } ``` - [ ] **Step 3: Add SetTemporaryParts method** Add as a public method on `PlateView`: ```csharp public void SetTemporaryParts(List parts) { temporaryParts.Clear(); if (parts != null) { foreach (var part in parts) temporaryParts.Add(LayoutPart.Create(part, this)); } Invalidate(); } ``` - [ ] **Step 4: Add ClearTemporaryParts method** ```csharp public void ClearTemporaryParts() { temporaryParts.Clear(); Invalidate(); } ``` - [ ] **Step 5: Add AcceptTemporaryParts method** This moves temporary parts into the real plate. It returns the count of parts accepted so the caller can use it for quantity tracking. ```csharp public int AcceptTemporaryParts() { var count = temporaryParts.Count; foreach (var layoutPart in temporaryParts) Plate.Parts.Add(layoutPart.BasePart); temporaryParts.Clear(); return count; } ``` Note: `Plate.Parts.Add()` fires `PartAdded` on `ObservableList`, which triggers `plate_PartAdded` on PlateView, which adds a new `LayoutPart` to the regular `parts` list. So the part transitions from temp → real automatically. - [ ] **Step 6: Clear temporary parts on plate switch** In `SetPlate` (line 116), add `temporaryParts.Clear()` in the cleanup section (after `parts.Clear()` at line 123): ```csharp temporaryParts.Clear(); ``` - [ ] **Step 7: Update Refresh to include temporary parts** In `Refresh()` (line 376), add temporary part updates: ```csharp public override void Refresh() { parts.ForEach(p => p.Update(this)); temporaryParts.ForEach(p => p.Update(this)); Invalidate(); } ``` - [ ] **Step 8: Build to verify** Run: `dotnet build OpenNest` Expected: Build succeeded - [ ] **Step 9: Commit** ```bash git add OpenNest/Controls/PlateView.cs git commit -m "feat(ui): add temporary parts list to PlateView with preview drawing" ``` --- ## Chunk 3: NestProgressForm ### Task 7: Create NestProgressForm **Files:** - Create: `OpenNest/Forms/NestProgressForm.cs` **Context:** This is a WinForms form. Since there's no designer in the CLI workflow, build it in code. The form is small and simple — a `TableLayoutPanel` with labels and a Stop button. - [ ] **Step 1: Create NestProgressForm.cs** ```csharp using System; using System.Drawing; using System.Threading; using System.Windows.Forms; namespace OpenNest.Forms { public class NestProgressForm : Form { private readonly CancellationTokenSource cts; private Label phaseValue; private Label plateValue; private Label partsValue; private Label densityValue; private Label remnantValue; private Label plateLabel; private Button stopButton; private TableLayoutPanel table; public NestProgressForm(CancellationTokenSource cts, bool showPlateRow = true) { this.cts = cts; InitializeLayout(showPlateRow); } private void InitializeLayout(bool showPlateRow) { Text = "Nesting Progress"; FormBorderStyle = FormBorderStyle.FixedToolWindow; StartPosition = FormStartPosition.CenterParent; ShowInTaskbar = false; MinimizeBox = false; MaximizeBox = false; Size = new Size(280, showPlateRow ? 210 : 190); table = new TableLayoutPanel { ColumnCount = 2, Dock = DockStyle.Top, AutoSize = true, Padding = new Padding(8) }; table.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 80)); table.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize)); phaseValue = AddRow(table, "Phase:"); plateValue = AddRow(table, "Plate:"); partsValue = AddRow(table, "Parts:"); densityValue = AddRow(table, "Density:"); remnantValue = AddRow(table, "Remnant:"); if (!showPlateRow) { plateLabel = FindLabel(table, "Plate:"); if (plateLabel != null) SetRowVisible(plateLabel, plateValue, false); } stopButton = new Button { Text = "Stop", Width = 80, Anchor = AnchorStyles.None, Margin = new Padding(0, 8, 0, 8) }; stopButton.Click += StopButton_Click; var buttonPanel = new FlowLayoutPanel { FlowDirection = FlowDirection.RightToLeft, Dock = DockStyle.Top, AutoSize = true, Padding = new Padding(8, 0, 8, 0) }; buttonPanel.Controls.Add(stopButton); Controls.Add(buttonPanel); Controls.Add(table); // Reverse order since Dock.Top stacks bottom-up Controls.SetChildIndex(table, 0); Controls.SetChildIndex(buttonPanel, 1); } private Label AddRow(TableLayoutPanel table, string labelText) { var row = table.RowCount; table.RowCount = row + 1; table.RowStyles.Add(new RowStyle(SizeType.AutoSize)); var label = new Label { Text = labelText, Font = new Font(Font, FontStyle.Bold), AutoSize = true, Margin = new Padding(4) }; var value = new Label { Text = "—", AutoSize = true, Margin = new Padding(4) }; table.Controls.Add(label, 0, row); table.Controls.Add(value, 1, row); return value; } private Label FindLabel(TableLayoutPanel table, string text) { foreach (Control c in table.Controls) { if (c is Label l && l.Text == text) return l; } return null; } private void SetRowVisible(Label label, Label value, bool visible) { label.Visible = visible; value.Visible = visible; } public void UpdateProgress(NestProgress progress) { if (IsDisposed || !IsHandleCreated) return; phaseValue.Text = FormatPhase(progress.Phase); plateValue.Text = progress.PlateNumber.ToString(); partsValue.Text = progress.BestPartCount.ToString(); densityValue.Text = progress.BestDensity.ToString("P1"); remnantValue.Text = $"{progress.UsableRemnantArea:F1} sq in"; } public void ShowCompleted() { if (IsDisposed || !IsHandleCreated) return; phaseValue.Text = "Done"; stopButton.Text = "Close"; stopButton.Enabled = true; stopButton.Click -= StopButton_Click; stopButton.Click += (s, e) => Close(); } private void StopButton_Click(object sender, EventArgs e) { cts.Cancel(); stopButton.Text = "Stopping..."; stopButton.Enabled = false; } protected override void OnFormClosing(FormClosingEventArgs e) { if (!cts.IsCancellationRequested) cts.Cancel(); base.OnFormClosing(e); } private static string FormatPhase(NestPhase phase) { switch (phase) { case NestPhase.Linear: return "Trying rotations..."; case NestPhase.RectBestFit: return "Trying best fit..."; case NestPhase.Pairs: return "Trying pairs..."; case NestPhase.Remainder: return "Filling remainder..."; default: return phase.ToString(); } } } } ``` - [ ] **Step 2: Build to verify** Run: `dotnet build OpenNest` Expected: Build succeeded - [ ] **Step 3: Commit** ```bash git add OpenNest/Forms/NestProgressForm.cs git commit -m "feat(ui): add NestProgressForm modeless dialog" ``` --- ## Chunk 4: MainForm Integration ### Task 8: Async RunAutoNest_Click **Files:** - Modify: `OpenNest/Forms/MainForm.cs` **Context:** `RunAutoNest_Click` is at line 692. It currently runs the nesting loop synchronously. The existing `EnableCheck()` pattern at line 107 is used for menu enable/disable. - [ ] **Step 1: Add a nesting-in-progress flag and lockout helper** Add fields near the top of `MainForm`: ```csharp private bool nestingInProgress; private CancellationTokenSource nestingCts; ``` Add a helper method that extends the existing `EnableCheck` pattern. Place it after `NavigationEnableCheck()`: ```csharp private void SetNestingLockout(bool locked) { nestingInProgress = locked; // Disable nesting-related menus while running mnuNest.Enabled = !locked; mnuPlate.Enabled = !locked; // Lock plate navigation mnuNestPreviousPlate.Enabled = !locked && activeForm != null && !activeForm.IsFirstPlate(); btnPreviousPlate.Enabled = mnuNestPreviousPlate.Enabled; mnuNestNextPlate.Enabled = !locked && activeForm != null && !activeForm.IsLastPlate(); btnNextPlate.Enabled = mnuNestNextPlate.Enabled; mnuNestFirstPlate.Enabled = !locked && activeForm != null && activeForm.PlateCount > 0 && !activeForm.IsFirstPlate(); btnFirstPlate.Enabled = mnuNestFirstPlate.Enabled; mnuNestLastPlate.Enabled = !locked && activeForm != null && activeForm.PlateCount > 0 && !activeForm.IsLastPlate(); btnLastPlate.Enabled = mnuNestLastPlate.Enabled; } ``` - [ ] **Step 2: Replace RunAutoNest_Click with async version** Replace the existing `RunAutoNest_Click` method (lines 692-738) with: ```csharp private async void RunAutoNest_Click(object sender, EventArgs e) { var form = new AutoNestForm(activeForm.Nest); form.AllowPlateCreation = true; if (form.ShowDialog() != System.Windows.Forms.DialogResult.OK) return; var items = form.GetNestItems(); if (!items.Any(it => it.Quantity > 0)) return; nestingCts = new CancellationTokenSource(); var token = nestingCts.Token; var progressForm = new NestProgressForm(nestingCts, showPlateRow: true); var plateNumber = 1; var progress = new Progress(p => { progressForm.UpdateProgress(p); activeForm.PlateView.SetTemporaryParts(p.BestParts); }); progressForm.Show(this); SetNestingLockout(true); try { while (items.Any(it => it.Quantity > 0)) { if (token.IsCancellationRequested) break; var plate = activeForm.PlateView.Plate.Parts.Count > 0 ? activeForm.Nest.CreatePlate() : activeForm.PlateView.Plate; // If a new plate was created, switch to it if (plate != activeForm.PlateView.Plate) activeForm.LoadLastPlate(); var engine = new NestEngine(plate) { PlateNumber = plateNumber }; var filled = false; foreach (var item in items) { if (item.Quantity <= 0) continue; if (token.IsCancellationRequested) break; // Run the engine on a background thread var parts = await Task.Run(() => engine.Fill(item, plate.WorkArea(), progress, token)); if (parts.Count == 0) continue; filled = true; // Count parts per drawing before accepting (for quantity tracking) foreach (var group in parts.GroupBy(p => p.BaseDrawing)) { var placed = group.Count(); foreach (var ni in items) { if (ni.Drawing == group.Key) ni.Quantity -= placed; } } // Accept the preview parts into the real plate activeForm.PlateView.AcceptTemporaryParts(); } if (!filled) break; plateNumber++; } activeForm.Nest.UpdateDrawingQuantities(); progressForm.ShowCompleted(); } catch (Exception ex) { activeForm.PlateView.ClearTemporaryParts(); MessageBox.Show($"Nesting error: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); } finally { progressForm.Close(); SetNestingLockout(false); nestingCts.Dispose(); nestingCts = null; } } ``` Add `using System.Threading.Tasks;` and `using System.Threading;` to the top of MainForm.cs if not already present. - [ ] **Step 3: Build to verify** Run: `dotnet build OpenNest` Expected: Build succeeded - [ ] **Step 4: Commit** ```bash git add OpenNest/Forms/MainForm.cs git commit -m "feat(ui): make RunAutoNest_Click async with progress and cancellation" ``` --- ### Task 9: Async FillPlate_Click **Files:** - Modify: `OpenNest/Forms/MainForm.cs` **Context:** `FillPlate_Click` is at line 785. Currently creates an engine and calls `Fill` synchronously. This should also show progress since a single-plate fill can be slow. - [ ] **Step 1: Replace FillPlate_Click with async version** Replace the existing `FillPlate_Click` method (lines 785-808) with: ```csharp private async void FillPlate_Click(object sender, EventArgs e) { if (activeForm == null) return; if (activeForm.Nest.Drawings.Count == 0) return; var form = new FillPlateForm(activeForm.Nest.Drawings); form.ShowDialog(); var drawing = form.SelectedDrawing; if (drawing == null) return; nestingCts = new CancellationTokenSource(); var token = nestingCts.Token; var progressForm = new NestProgressForm(nestingCts, showPlateRow: false); var progress = new Progress(p => { progressForm.UpdateProgress(p); activeForm.PlateView.SetTemporaryParts(p.BestParts); }); progressForm.Show(this); SetNestingLockout(true); try { var plate = activeForm.PlateView.Plate; var engine = new NestEngine(plate); var parts = await Task.Run(() => engine.Fill(new NestItem { Drawing = drawing }, plate.WorkArea(), progress, token)); if (parts.Count > 0) activeForm.PlateView.AcceptTemporaryParts(); else activeForm.PlateView.ClearTemporaryParts(); progressForm.ShowCompleted(); } catch (Exception ex) { activeForm.PlateView.ClearTemporaryParts(); MessageBox.Show($"Nesting error: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); } finally { progressForm.Close(); SetNestingLockout(false); nestingCts.Dispose(); nestingCts = null; } } ``` - [ ] **Step 2: Build to verify** Run: `dotnet build OpenNest` Expected: Build succeeded - [ ] **Step 3: Commit** ```bash git add OpenNest/Forms/MainForm.cs git commit -m "feat(ui): make FillPlate_Click async with progress and cancellation" ``` --- ### Task 10: ActionFillArea with Progress **Files:** - Modify: `OpenNest/Actions/ActionFillArea.cs` - Modify: `OpenNest/Forms/MainForm.cs` **Context:** `ActionFillArea` at `OpenNest/Actions/ActionFillArea.cs` currently creates an engine and calls `Fill` synchronously in its `FillArea()` method (line 28). Per the spec, the progress form is owned by MainForm, not ActionFillArea. ActionFillArea needs access to the progress/token infrastructure. The cleanest approach: add an event on ActionFillArea that MainForm listens to, letting MainForm handle the async orchestration. But that's overengineered for one call site. Instead, ActionFillArea can accept progress/token via its constructor and use the same async pattern. - [ ] **Step 1: Modify ActionFillArea to accept progress and cancellation** Replace the entire `ActionFillArea.cs`: ```csharp using System; using System.Collections.Generic; using System.ComponentModel; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; using OpenNest.Controls; namespace OpenNest.Actions { [DisplayName("Fill Area")] public class ActionFillArea : ActionSelectArea { private Drawing drawing; private IProgress progress; private CancellationTokenSource cts; private Action> onFillComplete; public ActionFillArea(PlateView plateView, Drawing drawing) : this(plateView, drawing, null, null, null) { } public ActionFillArea(PlateView plateView, Drawing drawing, IProgress progress, CancellationTokenSource cts, Action> onFillComplete) : base(plateView) { plateView.PreviewKeyDown += plateView_PreviewKeyDown; this.drawing = drawing; this.progress = progress; this.cts = cts; this.onFillComplete = onFillComplete; } private void plateView_PreviewKeyDown(object sender, PreviewKeyDownEventArgs e) { if (e.KeyCode == Keys.Enter) FillArea(); else if (e.KeyCode == Keys.Escape && cts != null) cts.Cancel(); } private async void FillArea() { if (progress != null && cts != null) { try { var engine = new NestEngine(plateView.Plate); var parts = await Task.Run(() => engine.Fill(new NestItem { Drawing = drawing }, SelectedArea, progress, cts.Token)); onFillComplete?.Invoke(parts); } catch (Exception) { onFillComplete?.Invoke(new List()); } } else { var engine = new NestEngine(plateView.Plate); engine.Fill(new NestItem { Drawing = drawing }, SelectedArea); plateView.Invalidate(); } Update(); } public override void DisconnectEvents() { plateView.PreviewKeyDown -= plateView_PreviewKeyDown; base.DisconnectEvents(); } } } ``` - [ ] **Step 2: Update FillArea_Click in MainForm to provide progress infrastructure** Replace `FillArea_Click` (line 810) in MainForm: ```csharp private void FillArea_Click(object sender, EventArgs e) { if (activeForm == null) return; if (activeForm.Nest.Drawings.Count == 0) return; var form = new FillPlateForm(activeForm.Nest.Drawings); form.ShowDialog(); var drawing = form.SelectedDrawing; if (drawing == null) return; nestingCts = new CancellationTokenSource(); var progressForm = new NestProgressForm(nestingCts, showPlateRow: false); var progress = new Progress(p => { progressForm.UpdateProgress(p); activeForm.PlateView.SetTemporaryParts(p.BestParts); }); Action> onComplete = parts => { if (parts != null && parts.Count > 0) activeForm.PlateView.AcceptTemporaryParts(); else activeForm.PlateView.ClearTemporaryParts(); progressForm.Close(); SetNestingLockout(false); nestingCts.Dispose(); nestingCts = null; }; progressForm.Show(this); SetNestingLockout(true); activeForm.PlateView.SetAction(typeof(ActionFillArea), drawing, progress, nestingCts, onComplete); } ``` - [ ] **Step 3: Verify PlateView.SetAction passes extra args to constructor** Check that `PlateView.SetAction(Type type, params object[] args)` passes args to the action constructor via reflection. Read the method to confirm it uses `Activator.CreateInstance` or similar with the extra params. If it prepends `PlateView` as the first arg, the constructor signature `(PlateView, Drawing, IProgress, CancellationTokenSource, Action)` should work with `args = [drawing, progress, nestingCts, onComplete]`. - [ ] **Step 4: Build to verify** Run: `dotnet build OpenNest` Expected: Build succeeded - [ ] **Step 5: Commit** ```bash git add OpenNest/Actions/ActionFillArea.cs OpenNest/Forms/MainForm.cs git commit -m "feat(ui): add progress support to ActionFillArea and FillArea_Click" ``` --- ## Chunk 5: Edge Cases and Cleanup ### Task 11: Handle EditNestForm Close During Nesting **Files:** - Modify: `OpenNest/Forms/MainForm.cs` **Context:** Per the spec, if the user closes the MDI child while nesting runs, cancel and close the progress form. `MainForm` already tracks `activeForm` and has an `OnMdiChildActivate` handler. - [ ] **Step 1: Cancel nesting on MDI child close** Find `OnMdiChildActivate` in MainForm (around line 290). Add a check when the active form changes or is set to null: In the section where `activeForm` is set, add: ```csharp // If nesting is in progress and the active form changed, cancel nesting if (nestingInProgress && nestingCts != null) { nestingCts.Cancel(); } ``` - [ ] **Step 2: Build to verify** Run: `dotnet build OpenNest` Expected: Build succeeded - [ ] **Step 3: Commit** ```bash git add OpenNest/Forms/MainForm.cs git commit -m "fix(ui): cancel nesting when MDI child form is closed" ``` --- ### Task 12: Final Build and Manual Test - [ ] **Step 1: Full solution build** Run: `dotnet build OpenNest.sln` Expected: Build succeeded, 0 warnings (or only pre-existing warnings) - [ ] **Step 2: Manual smoke test checklist** Launch the app and verify: 1. Open a nest with drawings 2. **Auto Nest** → shows progress form, parts appear on plate in preview color, stats update, Stop works, accepting gives real parts 3. **Fill Plate** → same as above for single plate 4. **Fill Area** → select area, press Enter, progress shows, Stop works 5. **Close MDI child during nesting** → nesting cancels cleanly 6. **Let nesting complete naturally** → progress form shows "Done", parts committed 7. **MCP tools still work** → the `bool Fill(...)` overloads are unchanged - [ ] **Step 3: Final commit if any fixes needed** ```bash git add -A git commit -m "fix: address issues found during manual testing" ```