1376 lines
42 KiB
Markdown
1376 lines
42 KiB
Markdown
# 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<Part>`-returning overloads that accept `IProgress<NestProgress>` + `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<T>`, `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<Part>`-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<Part> 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<NestProgress>` 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<T>`:
|
|
|
|
```csharp
|
|
private static void ReportProgress(
|
|
IProgress<NestProgress> progress,
|
|
NestPhase phase,
|
|
int plateNumber,
|
|
List<Part> best,
|
|
Box workArea)
|
|
{
|
|
if (progress == null || best == null || best.Count == 0)
|
|
return;
|
|
|
|
var score = FillScore.Compute(best, workArea);
|
|
var clonedParts = new List<Part>(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<Part> FindBestFill(NestItem item, Box workArea,
|
|
IProgress<NestProgress> progress, CancellationToken token)
|
|
{
|
|
List<Part> best = null;
|
|
|
|
try
|
|
{
|
|
var bestRotation = RotationAnalysis.FindBestRotation(item);
|
|
var engine = new FillLinear(workArea, Plate.PartSpacing);
|
|
var angles = new List<double> { 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<Part> 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<Part>();
|
|
}
|
|
```
|
|
|
|
- [ ] **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<Part> 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<Part> 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<Part> 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<Part>();
|
|
}
|
|
```
|
|
|
|
- [ ] **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<Part>`-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<Part> 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<Part> Fill(NestItem item, Box workArea,
|
|
IProgress<NestProgress> progress, CancellationToken token)
|
|
{
|
|
var best = FindBestFill(item, workArea, progress, token);
|
|
|
|
if (token.IsCancellationRequested)
|
|
return best ?? new List<Part>();
|
|
|
|
// 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<Part>();
|
|
|
|
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\<Part\>, Box, IProgress, CancellationToken) overload**
|
|
|
|
Add after the existing `Fill(List<Part> groupParts, Box workArea)` method (after line 180). Same pattern — compute and return without touching `Plate.Parts`:
|
|
|
|
```csharp
|
|
public List<Part> Fill(List<Part> groupParts, Box workArea,
|
|
IProgress<NestProgress> progress, CancellationToken token)
|
|
{
|
|
if (groupParts == null || groupParts.Count == 0)
|
|
return new List<Part>();
|
|
|
|
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<Part>();
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 4: Rewire existing bool Fill(List\<Part\>, Box) to delegate**
|
|
|
|
Replace the body of `Fill(List<Part> groupParts, Box workArea)` at line 137:
|
|
|
|
```csharp
|
|
public bool Fill(List<Part> 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<Part> 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<LayoutPart>`) 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<LayoutPart> temporaryParts = new List<LayoutPart>();
|
|
```
|
|
|
|
- [ ] **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<Part> 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<NestProgress>(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<NestProgress>(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<NestProgress> progress;
|
|
private CancellationTokenSource cts;
|
|
private Action<List<Part>> onFillComplete;
|
|
|
|
public ActionFillArea(PlateView plateView, Drawing drawing)
|
|
: this(plateView, drawing, null, null, null)
|
|
{
|
|
}
|
|
|
|
public ActionFillArea(PlateView plateView, Drawing drawing,
|
|
IProgress<NestProgress> progress, CancellationTokenSource cts,
|
|
Action<List<Part>> 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<Part>());
|
|
}
|
|
}
|
|
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<NestProgress>(p =>
|
|
{
|
|
progressForm.UpdateProgress(p);
|
|
activeForm.PlateView.SetTemporaryParts(p.BestParts);
|
|
});
|
|
|
|
Action<List<Part>> 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"
|
|
```
|