Files
OpenNest/docs/superpowers/plans/2026-03-18-two-bucket-preview.md
AJ Isaacs 0f953b8701 docs: add two-bucket preview spec and plan
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 22:48:25 -04:00

15 KiB

Two-Bucket Preview Parts Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Split PlateView nesting preview into stationary (overall best) and active (current strategy) layers so the preview never regresses and acceptance always uses the engine's result.

Architecture: Add IsOverallBest flag to NestProgress so the engine can distinguish overall-best reports from strategy-local progress. PlateView maintains two List<LayoutPart> buckets drawn at different opacities. Acceptance uses the engine's returned parts directly, decoupling preview from acceptance.

Tech Stack: C# / .NET 8 / WinForms / System.Drawing

Spec: docs/superpowers/specs/2026-03-18-two-bucket-preview-design.md


Task 1: Add IsOverallBest to NestProgress and ReportProgress

Files:

  • Modify: OpenNest.Engine/NestProgress.cs:37-49

  • Modify: OpenNest.Engine/NestEngineBase.cs:188-236

  • Step 1: Add property to NestProgress

In OpenNest.Engine/NestProgress.cs, add after line 48 (ActiveWorkArea):

public bool IsOverallBest { get; set; }
  • Step 2: Add parameter to ReportProgress

In OpenNest.Engine/NestEngineBase.cs, change the ReportProgress signature (line 188) to:

internal static void ReportProgress(
    IProgress<NestProgress> progress,
    NestPhase phase,
    int plateNumber,
    List<Part> best,
    Box workArea,
    string description,
    bool isOverallBest = false)

In the same method, add IsOverallBest = isOverallBest to the NestProgress initializer (after line 235 ActiveWorkArea = workArea):

IsOverallBest = isOverallBest,
  • Step 3: Build to verify

Run: dotnet build OpenNest.Engine/OpenNest.Engine.csproj Expected: Build succeeded, 0 errors. Existing callers use the default false.

  • Step 4: Commit
feat(engine): add IsOverallBest flag to NestProgress

Task 2: Flag overall-best reports in DefaultNestEngine

Files:

  • Modify: OpenNest.Engine/DefaultNestEngine.cs:55-58 (final report in Fill(NestItem))

  • Modify: OpenNest.Engine/DefaultNestEngine.cs:83-85 (final report in Fill(List))

  • Modify: OpenNest.Engine/DefaultNestEngine.cs:132-139 (RunPipeline strategy loop)

  • Step 1: Update RunPipeline — replace conditional report with unconditional overall-best report

In RunPipeline (line 132-139), change from:

if (IsBetterFill(result, context.CurrentBest, context.WorkArea))
{
    context.CurrentBest = result;
    context.CurrentBestScore = FillScore.Compute(result, context.WorkArea);
    context.WinnerPhase = strategy.Phase;
    ReportProgress(context.Progress, strategy.Phase, PlateNumber,
        result, context.WorkArea, BuildProgressSummary());
}

to:

if (IsBetterFill(result, context.CurrentBest, context.WorkArea))
{
    context.CurrentBest = result;
    context.CurrentBestScore = FillScore.Compute(result, context.WorkArea);
    context.WinnerPhase = strategy.Phase;
}

if (context.CurrentBest != null && context.CurrentBest.Count > 0)
{
    ReportProgress(context.Progress, context.WinnerPhase, PlateNumber,
        context.CurrentBest, context.WorkArea, BuildProgressSummary(),
        isOverallBest: true);
}
  • Step 2: Flag final report in Fill(NestItem, Box, ...)

In Fill(NestItem item, Box workArea, ...) (line 58), change:

ReportProgress(progress, WinnerPhase, PlateNumber, best, workArea, BuildProgressSummary());

to:

ReportProgress(progress, WinnerPhase, PlateNumber, best, workArea, BuildProgressSummary(),
    isOverallBest: true);
  • Step 3: Flag final report in Fill(List, Box, ...)

In Fill(List<Part> groupParts, Box workArea, ...) (line 85), change:

ReportProgress(progress, NestPhase.Linear, PlateNumber, best, workArea, BuildProgressSummary());

to:

ReportProgress(progress, NestPhase.Linear, PlateNumber, best, workArea, BuildProgressSummary(),
    isOverallBest: true);
  • Step 4: Build to verify

Run: dotnet build OpenNest.Engine/OpenNest.Engine.csproj Expected: Build succeeded, 0 errors.

  • Step 5: Commit
feat(engine): flag overall-best progress reports in DefaultNestEngine

Task 3: Add active preview style to ColorScheme

Files:

  • Modify: OpenNest/ColorScheme.cs:58-61 (pen/brush declarations)

  • Modify: OpenNest/ColorScheme.cs:160-176 (PreviewPartColor setter)

  • Step 1: Add pen/brush declarations

In ColorScheme.cs, after line 60 (PreviewPartBrush), add:

public Pen ActivePreviewPartPen { get; private set; }

public Brush ActivePreviewPartBrush { get; private set; }
  • Step 2: Create resources in PreviewPartColor setter

In the PreviewPartColor setter (lines 160-176), change from:

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));
}

to:

set
{
    previewPartColor = value;

    if (PreviewPartPen != null)
        PreviewPartPen.Dispose();

    if (PreviewPartBrush != null)
        PreviewPartBrush.Dispose();

    if (ActivePreviewPartPen != null)
        ActivePreviewPartPen.Dispose();

    if (ActivePreviewPartBrush != null)
        ActivePreviewPartBrush.Dispose();

    PreviewPartPen = new Pen(value, 1);
    PreviewPartBrush = new SolidBrush(Color.FromArgb(60, value));
    ActivePreviewPartPen = new Pen(Color.FromArgb(128, value), 1);
    ActivePreviewPartBrush = new SolidBrush(Color.FromArgb(30, value));
}
  • Step 3: Build to verify

Run: dotnet build OpenNest/OpenNest.csproj Expected: Build succeeded, 0 errors.

  • Step 4: Commit
feat(ui): add active preview brush/pen to ColorScheme

Task 4: Two-bucket preview parts in PlateView

Files:

  • Modify: OpenNest/Controls/PlateView.cs

This task replaces the single temporaryParts list with stationaryParts and activeParts, updates the public API, drawing, and all internal references.

  • Step 1: Replace field and add new list

Change line 34:

private List<LayoutPart> temporaryParts = new List<LayoutPart>();

to:

private List<LayoutPart> stationaryParts = new List<LayoutPart>();
private List<LayoutPart> activeParts = new List<LayoutPart>();
  • Step 2: Update SetPlate (line 152-153)

Change:

temporaryParts.Clear();

to:

stationaryParts.Clear();
activeParts.Clear();
  • Step 3: Update Refresh (line 411)

Change:

temporaryParts.ForEach(p => p.Update(this));

to:

stationaryParts.ForEach(p => p.Update(this));
activeParts.ForEach(p => p.Update(this));
  • Step 4: Update UpdateMatrix (line 1085)

Change:

temporaryParts.ForEach(p => p.Update(this));

to:

stationaryParts.ForEach(p => p.Update(this));
activeParts.ForEach(p => p.Update(this));
  • Step 5: Replace the temporary parts drawing block in DrawParts (lines 506-522)

Change:

// 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);
}

to:

// Draw stationary preview parts (overall best — full opacity)
for (var i = 0; i < stationaryParts.Count; i++)
{
    var part = stationaryParts[i];

    if (part.IsDirty)
        part.Update(this);

    var path = part.Path;
    if (!path.GetBounds().IntersectsWith(viewBounds))
        continue;

    g.FillPath(ColorScheme.PreviewPartBrush, path);
    g.DrawPath(ColorScheme.PreviewPartPen, path);
}

// Draw active preview parts (current strategy — reduced opacity)
for (var i = 0; i < activeParts.Count; i++)
{
    var part = activeParts[i];

    if (part.IsDirty)
        part.Update(this);

    var path = part.Path;
    if (!path.GetBounds().IntersectsWith(viewBounds))
        continue;

    g.FillPath(ColorScheme.ActivePreviewPartBrush, path);
    g.DrawPath(ColorScheme.ActivePreviewPartPen, path);
}
  • Step 6: Replace public API methods (lines 882-910)

Replace SetTemporaryParts, ClearTemporaryParts, and AcceptTemporaryParts with:

public void SetStationaryParts(List<Part> parts)
{
    stationaryParts.Clear();

    if (parts != null)
    {
        foreach (var part in parts)
            stationaryParts.Add(LayoutPart.Create(part, this));
    }

    Invalidate();
}

public void SetActiveParts(List<Part> parts)
{
    activeParts.Clear();

    if (parts != null)
    {
        foreach (var part in parts)
            activeParts.Add(LayoutPart.Create(part, this));
    }

    Invalidate();
}

public void ClearPreviewParts()
{
    stationaryParts.Clear();
    activeParts.Clear();
    Invalidate();
}

public void AcceptPreviewParts(List<Part> parts)
{
    if (parts != null)
    {
        foreach (var part in parts)
            Plate.Parts.Add(part);
    }

    stationaryParts.Clear();
    activeParts.Clear();
}
  • Step 7: Update FillWithProgress (lines 912-957)

Change the progress callback (lines 918-923):

var progress = new Progress<NestProgress>(p =>
{
    progressForm.UpdateProgress(p);
    SetTemporaryParts(p.BestParts);
    ActiveWorkArea = p.ActiveWorkArea;
});

to:

var progress = new Progress<NestProgress>(p =>
{
    progressForm.UpdateProgress(p);

    if (p.IsOverallBest)
        SetStationaryParts(p.BestParts);
    else
        SetActiveParts(p.BestParts);

    ActiveWorkArea = p.ActiveWorkArea;
});

Change the acceptance block (lines 933-943):

if (parts.Count > 0 && (!cts.IsCancellationRequested || progressForm.Accepted))
{
    SetTemporaryParts(parts);
    AcceptTemporaryParts();
    sw.Stop();
    Status = $"Fill: {parts.Count} parts in {sw.ElapsedMilliseconds} ms";
}
else
{
    ClearTemporaryParts();
}

to:

if (parts.Count > 0 && (!cts.IsCancellationRequested || progressForm.Accepted))
{
    AcceptPreviewParts(parts);
    sw.Stop();
    Status = $"Fill: {parts.Count} parts in {sw.ElapsedMilliseconds} ms";
}
else
{
    ClearPreviewParts();
}

Change the catch block (line 949):

ClearTemporaryParts();

to:

ClearPreviewParts();
  • Step 8: Build to verify

Run: dotnet build OpenNest/OpenNest.csproj Expected: Build errors in MainForm.cs (still references old API). That is expected — Task 5 fixes it.

  • Step 9: Commit
feat(ui): two-bucket preview parts in PlateView

Task 5: Update MainForm progress callbacks and acceptance

Files:

  • Modify: OpenNest/Forms/MainForm.cs

Three progress callback sites and their acceptance points need updating.

  • Step 1: Update auto-nest callback (RunAutoNest_Click, line 827)

Change:

var progress = new Progress<NestProgress>(p =>
{
    progressForm.UpdateProgress(p);
    activeForm.PlateView.SetTemporaryParts(p.BestParts);
    activeForm.PlateView.ActiveWorkArea = p.ActiveWorkArea;
});

to:

var progress = new Progress<NestProgress>(p =>
{
    progressForm.UpdateProgress(p);

    if (p.IsOverallBest)
        activeForm.PlateView.SetStationaryParts(p.BestParts);
    else
        activeForm.PlateView.SetActiveParts(p.BestParts);

    activeForm.PlateView.ActiveWorkArea = p.ActiveWorkArea;
});

Change ClearTemporaryParts() on line 866 to ClearPreviewParts().

Change ClearTemporaryParts() in the catch block (line 884) to ClearPreviewParts().

  • Step 2: Update fill-plate callback (FillPlate_Click, line 962)

Replace the progress setup (lines 962-976):

var progressForm = new NestProgressForm(nestingCts, showPlateRow: false);
var highWaterMark = 0;

var progress = new Progress<NestProgress>(p =>
{
    progressForm.UpdateProgress(p);

    if (p.BestParts != null && p.BestPartCount >= highWaterMark)
    {
        highWaterMark = p.BestPartCount;
        activeForm.PlateView.SetTemporaryParts(p.BestParts);
    }

    activeForm.PlateView.ActiveWorkArea = p.ActiveWorkArea;
});

with:

var progressForm = new NestProgressForm(nestingCts, showPlateRow: false);

var progress = new Progress<NestProgress>(p =>
{
    progressForm.UpdateProgress(p);

    if (p.IsOverallBest)
        activeForm.PlateView.SetStationaryParts(p.BestParts);
    else
        activeForm.PlateView.SetActiveParts(p.BestParts);

    activeForm.PlateView.ActiveWorkArea = p.ActiveWorkArea;
});

Change acceptance (line 990-993):

if (parts.Count > 0)
    activeForm.PlateView.AcceptTemporaryParts();
else
    activeForm.PlateView.ClearTemporaryParts();

to:

if (parts.Count > 0)
    activeForm.PlateView.AcceptPreviewParts(parts);
else
    activeForm.PlateView.ClearPreviewParts();

Change ClearTemporaryParts() in the catch block to ClearPreviewParts().

  • Step 3: Update fill-area callback (FillArea_Click, line 1031)

Replace the progress setup (lines 1031-1045):

var progressForm = new NestProgressForm(nestingCts, showPlateRow: false);
var highWaterMark = 0;

var progress = new Progress<NestProgress>(p =>
{
    progressForm.UpdateProgress(p);

    if (p.BestParts != null && p.BestPartCount >= highWaterMark)
    {
        highWaterMark = p.BestPartCount;
        activeForm.PlateView.SetTemporaryParts(p.BestParts);
    }

    activeForm.PlateView.ActiveWorkArea = p.ActiveWorkArea;
});

with:

var progressForm = new NestProgressForm(nestingCts, showPlateRow: false);

var progress = new Progress<NestProgress>(p =>
{
    progressForm.UpdateProgress(p);

    if (p.IsOverallBest)
        activeForm.PlateView.SetStationaryParts(p.BestParts);
    else
        activeForm.PlateView.SetActiveParts(p.BestParts);

    activeForm.PlateView.ActiveWorkArea = p.ActiveWorkArea;
});

Change the onComplete callback (lines 1047-1052):

Action<List<Part>> onComplete = parts =>
{
    if (parts != null && parts.Count > 0)
        activeForm.PlateView.AcceptTemporaryParts();
    else
        activeForm.PlateView.ClearTemporaryParts();

to:

Action<List<Part>> onComplete = parts =>
{
    if (parts != null && parts.Count > 0)
        activeForm.PlateView.AcceptPreviewParts(parts);
    else
        activeForm.PlateView.ClearPreviewParts();
  • Step 4: Build full solution

Run: dotnet build OpenNest.sln Expected: Build succeeded, 0 errors (only pre-existing nullable warnings in OpenNest.Gpu).

  • Step 5: Run tests

Run: dotnet test OpenNest.Tests/OpenNest.Tests.csproj Expected: All tests pass.

  • Step 6: Commit
feat(ui): route progress to stationary/active buckets in MainForm