From a1810db96d953574bd018a6a8a913436eb578b33 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Wed, 18 Mar 2026 17:22:49 -0400 Subject: [PATCH] docs: add NestProgressForm redesign v2 implementation plan 6-task plan covering PhaseStepperControl, DensityBar, form rewrite, color-coded flash & fade, Accept/Stop buttons, and caller changes. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../2026-03-18-progress-form-redesign-v2.md | 1015 +++++++++++++++++ 1 file changed, 1015 insertions(+) create mode 100644 docs/superpowers/plans/2026-03-18-progress-form-redesign-v2.md diff --git a/docs/superpowers/plans/2026-03-18-progress-form-redesign-v2.md b/docs/superpowers/plans/2026-03-18-progress-form-redesign-v2.md new file mode 100644 index 0000000..71ab4ab --- /dev/null +++ b/docs/superpowers/plans/2026-03-18-progress-form-redesign-v2.md @@ -0,0 +1,1015 @@ +# NestProgressForm Redesign v2 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:** Redesign the NestProgressForm with a phase stepper, grouped panels, density sparkline, color-coded flash & fade, and Accept/Stop buttons. + +**Architecture:** Four new/modified components: PhaseStepperControl (owner-drawn phase circles), DensityBar (owner-drawn sparkline), NestProgressForm (rewritten layout integrating both controls with grouped panels and dual buttons), and caller changes for Accept support. All controls use WinForms owner-draw with cached fonts/brushes. + +**Tech Stack:** C# .NET 8 WinForms, System.Drawing for GDI+ rendering + +**Spec:** `docs/superpowers/specs/2026-03-18-progress-form-redesign-v2-design.md` + +--- + +### Task 1: PhaseStepperControl + +**Files:** +- Create: `OpenNest/Controls/PhaseStepperControl.cs` + +- [ ] **Step 1: Create the PhaseStepperControl** + +```csharp +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Windows.Forms; + +namespace OpenNest.Controls +{ + public class PhaseStepperControl : UserControl + { + private static readonly Color AccentColor = Color.FromArgb(0, 120, 212); + private static readonly Color GlowColor = Color.FromArgb(60, 0, 120, 212); + private static readonly Color PendingBorder = Color.FromArgb(192, 192, 192); + private static readonly Color LineColor = Color.FromArgb(208, 208, 208); + private static readonly Color PendingTextColor = Color.FromArgb(153, 153, 153); + private static readonly Color ActiveTextColor = Color.FromArgb(51, 51, 51); + + private static readonly Font LabelFont = new Font("Segoe UI", 8f, FontStyle.Regular); + private static readonly Font BoldLabelFont = new Font("Segoe UI", 8f, FontStyle.Bold); + + private static readonly NestPhase[] Phases = (NestPhase[])Enum.GetValues(typeof(NestPhase)); + + private readonly HashSet visitedPhases = new(); + private NestPhase? activePhase; + private bool isComplete; + + public PhaseStepperControl() + { + DoubleBuffered = true; + SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint, true); + Height = 60; + } + + public NestPhase? ActivePhase + { + get => activePhase; + set + { + activePhase = value; + if (value.HasValue) + visitedPhases.Add(value.Value); + Invalidate(); + } + } + + public bool IsComplete + { + get => isComplete; + set + { + isComplete = value; + if (value) + { + foreach (var phase in Phases) + visitedPhases.Add(phase); + activePhase = null; + } + Invalidate(); + } + } + + private static string GetDisplayName(NestPhase phase) + { + switch (phase) + { + case NestPhase.RectBestFit: return "BestFit"; + case NestPhase.Nfp: return "NFP"; + default: return phase.ToString(); + } + } + + protected override void OnPaint(PaintEventArgs e) + { + base.OnPaint(e); + var g = e.Graphics; + g.SmoothingMode = SmoothingMode.AntiAlias; + g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit; + + var count = Phases.Length; + if (count == 0) return; + + var padding = 30; + var usableWidth = Width - padding * 2; + var spacing = usableWidth / (count - 1); + var circleY = 18; + var normalRadius = 9; + var activeRadius = 11; + + using var linePen = new Pen(LineColor, 2f); + using var accentBrush = new SolidBrush(AccentColor); + using var glowBrush = new SolidBrush(GlowColor); + using var pendingPen = new Pen(PendingBorder, 2f); + using var activeTextBrush = new SolidBrush(ActiveTextColor); + using var pendingTextBrush = new SolidBrush(PendingTextColor); + + // Draw connecting lines + for (var i = 0; i < count - 1; i++) + { + var x1 = padding + i * spacing; + var x2 = padding + (i + 1) * spacing; + g.DrawLine(linePen, x1, circleY, x2, circleY); + } + + // Draw circles and labels + for (var i = 0; i < count; i++) + { + var phase = Phases[i]; + var cx = padding + i * spacing; + var isActive = activePhase == phase && !isComplete; + var isVisited = visitedPhases.Contains(phase) || isComplete; + + if (isActive) + { + // Glow + g.FillEllipse(glowBrush, + cx - activeRadius - 3, circleY - activeRadius - 3, + (activeRadius + 3) * 2, (activeRadius + 3) * 2); + // Filled circle + g.FillEllipse(accentBrush, + cx - activeRadius, circleY - activeRadius, + activeRadius * 2, activeRadius * 2); + } + else if (isVisited) + { + g.FillEllipse(accentBrush, + cx - normalRadius, circleY - normalRadius, + normalRadius * 2, normalRadius * 2); + } + else + { + g.DrawEllipse(pendingPen, + cx - normalRadius, circleY - normalRadius, + normalRadius * 2, normalRadius * 2); + } + + // Label + var label = GetDisplayName(phase); + var font = isVisited || isActive ? BoldLabelFont : LabelFont; + var brush = isVisited || isActive ? activeTextBrush : pendingTextBrush; + var labelSize = g.MeasureString(label, font); + g.DrawString(label, font, brush, + cx - labelSize.Width / 2, circleY + activeRadius + 5); + } + } + } +} +``` + +- [ ] **Step 2: Build to verify** + +Run: `dotnet build OpenNest/OpenNest.csproj --no-restore -v q` +Expected: 0 errors + +- [ ] **Step 3: Commit** + +```bash +git add OpenNest/Controls/PhaseStepperControl.cs +git commit -m "feat(ui): add PhaseStepperControl for nesting progress phases" +``` + +--- + +### Task 2: DensityBar Control + +**Files:** +- Create: `OpenNest/Controls/DensityBar.cs` + +- [ ] **Step 1: Create the DensityBar control** + +```csharp +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Windows.Forms; + +namespace OpenNest.Controls +{ + public class DensityBar : Control + { + private static readonly Color TrackColor = Color.FromArgb(224, 224, 224); + private static readonly Color LowColor = Color.FromArgb(245, 166, 35); + private static readonly Color HighColor = Color.FromArgb(76, 175, 80); + + private double value; + + public DensityBar() + { + DoubleBuffered = true; + SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint, true); + Size = new Size(60, 8); + } + + public double Value + { + get => value; + set + { + this.value = System.Math.Clamp(value, 0.0, 1.0); + Invalidate(); + } + } + + protected override void OnPaint(PaintEventArgs e) + { + base.OnPaint(e); + var g = e.Graphics; + g.SmoothingMode = SmoothingMode.AntiAlias; + + var rect = new Rectangle(0, 0, Width - 1, Height - 1); + + // Track background + using var trackPath = CreateRoundedRect(rect, 4); + using var trackBrush = new SolidBrush(TrackColor); + g.FillPath(trackBrush, trackPath); + + // Fill + var fillWidth = (int)(rect.Width * value); + if (fillWidth > 0) + { + var fillRect = new Rectangle(rect.X, rect.Y, fillWidth, rect.Height); + using var fillPath = CreateRoundedRect(fillRect, 4); + using var gradientBrush = new LinearGradientBrush( + new Point(rect.X, 0), new Point(rect.Right, 0), + LowColor, HighColor); + g.FillPath(gradientBrush, fillPath); + } + } + + private static GraphicsPath CreateRoundedRect(Rectangle rect, int radius) + { + var path = new GraphicsPath(); + var d = radius * 2; + path.AddArc(rect.X, rect.Y, d, d, 180, 90); + path.AddArc(rect.Right - d, rect.Y, d, d, 270, 90); + path.AddArc(rect.Right - d, rect.Bottom - d, d, d, 0, 90); + path.AddArc(rect.X, rect.Bottom - d, d, d, 90, 90); + path.CloseFigure(); + return path; + } + } +} +``` + +- [ ] **Step 2: Build to verify** + +Run: `dotnet build OpenNest/OpenNest.csproj --no-restore -v q` +Expected: 0 errors + +- [ ] **Step 3: Commit** + +```bash +git add OpenNest/Controls/DensityBar.cs +git commit -m "feat(ui): add DensityBar sparkline control for density visualization" +``` + +--- + +### Task 3: Rewrite NestProgressForm Designer + +**Files:** +- Modify: `OpenNest/Forms/NestProgressForm.Designer.cs` (full rewrite) + +The designer file creates the grouped layout: phase stepper at top, Results panel (Parts, Density + DensityBar, Nested), Status panel (Plate, Elapsed, Detail), and button bar (Accept + Stop). + +- [ ] **Step 1: Rewrite the designer** + +Replace the entire `InitializeComponent` method and field declarations. Key changes: +- Remove `phaseLabel`, `phaseValue`, `remnantLabel`, `remnantValue`, and the single flat `table` +- Add `phaseStepper` (PhaseStepperControl) +- Add `resultsPanel`, `statusPanel` (white Panels with header labels) +- Add `resultsTable`, `statusTable` (TableLayoutPanels inside panels) +- Add `densityBar` (DensityBar control, placed in density row) +- Add `acceptButton` alongside `stopButton` +- Fonts: Segoe UI 9pt bold for headers, 8.25pt bold for row labels, Consolas 8.25pt for values +- Form `ClientSize`: 450 x 315 + +Full designer code: + +```csharp +namespace OpenNest.Forms +{ + partial class NestProgressForm + { + private System.ComponentModel.IContainer components = null; + + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + private void InitializeComponent() + { + phaseStepper = new Controls.PhaseStepperControl(); + resultsPanel = new System.Windows.Forms.Panel(); + resultsHeader = new System.Windows.Forms.Label(); + resultsTable = new System.Windows.Forms.TableLayoutPanel(); + partsLabel = new System.Windows.Forms.Label(); + partsValue = new System.Windows.Forms.Label(); + densityLabel = new System.Windows.Forms.Label(); + densityPanel = new System.Windows.Forms.FlowLayoutPanel(); + densityValue = new System.Windows.Forms.Label(); + densityBar = new Controls.DensityBar(); + nestedAreaLabel = new System.Windows.Forms.Label(); + nestedAreaValue = new System.Windows.Forms.Label(); + statusPanel = new System.Windows.Forms.Panel(); + statusHeader = new System.Windows.Forms.Label(); + statusTable = new System.Windows.Forms.TableLayoutPanel(); + plateLabel = new System.Windows.Forms.Label(); + plateValue = new System.Windows.Forms.Label(); + elapsedLabel = new System.Windows.Forms.Label(); + elapsedValue = new System.Windows.Forms.Label(); + descriptionLabel = new System.Windows.Forms.Label(); + descriptionValue = new System.Windows.Forms.Label(); + buttonPanel = new System.Windows.Forms.FlowLayoutPanel(); + acceptButton = new System.Windows.Forms.Button(); + stopButton = new System.Windows.Forms.Button(); + + resultsPanel.SuspendLayout(); + resultsTable.SuspendLayout(); + densityPanel.SuspendLayout(); + statusPanel.SuspendLayout(); + statusTable.SuspendLayout(); + buttonPanel.SuspendLayout(); + SuspendLayout(); + + // + // phaseStepper + // + phaseStepper.Dock = System.Windows.Forms.DockStyle.Top; + phaseStepper.Height = 60; + phaseStepper.Name = "phaseStepper"; + phaseStepper.TabIndex = 0; + + // + // resultsPanel + // + resultsPanel.BackColor = System.Drawing.Color.White; + resultsPanel.Controls.Add(resultsTable); + resultsPanel.Controls.Add(resultsHeader); + resultsPanel.Dock = System.Windows.Forms.DockStyle.Top; + resultsPanel.Location = new System.Drawing.Point(0, 60); + resultsPanel.Margin = new System.Windows.Forms.Padding(10, 4, 10, 4); + resultsPanel.Name = "resultsPanel"; + resultsPanel.Padding = new System.Windows.Forms.Padding(14, 10, 14, 10); + resultsPanel.Size = new System.Drawing.Size(450, 105); + resultsPanel.TabIndex = 1; + + // + // resultsHeader + // + resultsHeader.AutoSize = true; + resultsHeader.Dock = System.Windows.Forms.DockStyle.Top; + resultsHeader.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Bold); + resultsHeader.ForeColor = System.Drawing.Color.FromArgb(85, 85, 85); + resultsHeader.Name = "resultsHeader"; + resultsHeader.Padding = new System.Windows.Forms.Padding(0, 0, 0, 4); + resultsHeader.Size = new System.Drawing.Size(63, 19); + resultsHeader.TabIndex = 0; + resultsHeader.Text = "RESULTS"; + + // + // resultsTable + // + resultsTable.AutoSize = true; + resultsTable.ColumnCount = 2; + resultsTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 80F)); + resultsTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); + resultsTable.Controls.Add(partsLabel, 0, 0); + resultsTable.Controls.Add(partsValue, 1, 0); + resultsTable.Controls.Add(densityLabel, 0, 1); + resultsTable.Controls.Add(densityPanel, 1, 1); + resultsTable.Controls.Add(nestedAreaLabel, 0, 2); + resultsTable.Controls.Add(nestedAreaValue, 1, 2); + resultsTable.Dock = System.Windows.Forms.DockStyle.Top; + resultsTable.Name = "resultsTable"; + resultsTable.RowCount = 3; + resultsTable.RowStyles.Add(new System.Windows.Forms.RowStyle()); + resultsTable.RowStyles.Add(new System.Windows.Forms.RowStyle()); + resultsTable.RowStyles.Add(new System.Windows.Forms.RowStyle()); + resultsTable.TabIndex = 1; + + // + // partsLabel + // + partsLabel.AutoSize = true; + partsLabel.Font = new System.Drawing.Font("Segoe UI", 8.25F, System.Drawing.FontStyle.Bold); + partsLabel.ForeColor = System.Drawing.Color.FromArgb(51, 51, 51); + partsLabel.Margin = new System.Windows.Forms.Padding(0, 3, 5, 3); + partsLabel.Name = "partsLabel"; + partsLabel.Text = "Parts:"; + + // + // partsValue + // + partsValue.AutoSize = true; + partsValue.Font = new System.Drawing.Font("Consolas", 8.25F); + partsValue.Margin = new System.Windows.Forms.Padding(0, 3, 0, 3); + partsValue.Name = "partsValue"; + partsValue.Text = "\u2014"; + + // + // densityLabel + // + densityLabel.AutoSize = true; + densityLabel.Font = new System.Drawing.Font("Segoe UI", 8.25F, System.Drawing.FontStyle.Bold); + densityLabel.ForeColor = System.Drawing.Color.FromArgb(51, 51, 51); + densityLabel.Margin = new System.Windows.Forms.Padding(0, 3, 5, 3); + densityLabel.Name = "densityLabel"; + densityLabel.Text = "Density:"; + + // + // densityPanel + // + densityPanel.AutoSize = true; + densityPanel.Controls.Add(densityValue); + densityPanel.Controls.Add(densityBar); + densityPanel.FlowDirection = System.Windows.Forms.FlowDirection.LeftToRight; + densityPanel.Margin = new System.Windows.Forms.Padding(0); + densityPanel.Name = "densityPanel"; + densityPanel.WrapContents = false; + + // + // densityValue + // + densityValue.AutoSize = true; + densityValue.Font = new System.Drawing.Font("Consolas", 8.25F); + densityValue.Margin = new System.Windows.Forms.Padding(0, 3, 8, 3); + densityValue.Name = "densityValue"; + densityValue.Text = "\u2014"; + + // + // densityBar + // + densityBar.Margin = new System.Windows.Forms.Padding(0, 5, 0, 0); + densityBar.Name = "densityBar"; + densityBar.Size = new System.Drawing.Size(60, 8); + + // + // nestedAreaLabel + // + nestedAreaLabel.AutoSize = true; + nestedAreaLabel.Font = new System.Drawing.Font("Segoe UI", 8.25F, System.Drawing.FontStyle.Bold); + nestedAreaLabel.ForeColor = System.Drawing.Color.FromArgb(51, 51, 51); + nestedAreaLabel.Margin = new System.Windows.Forms.Padding(0, 3, 5, 3); + nestedAreaLabel.Name = "nestedAreaLabel"; + nestedAreaLabel.Text = "Nested:"; + + // + // nestedAreaValue + // + nestedAreaValue.AutoSize = true; + nestedAreaValue.Font = new System.Drawing.Font("Consolas", 8.25F); + nestedAreaValue.Margin = new System.Windows.Forms.Padding(0, 3, 0, 3); + nestedAreaValue.Name = "nestedAreaValue"; + nestedAreaValue.Text = "\u2014"; + + // + // statusPanel + // + statusPanel.BackColor = System.Drawing.Color.White; + statusPanel.Controls.Add(statusTable); + statusPanel.Controls.Add(statusHeader); + statusPanel.Dock = System.Windows.Forms.DockStyle.Top; + statusPanel.Location = new System.Drawing.Point(0, 169); + statusPanel.Name = "statusPanel"; + statusPanel.Padding = new System.Windows.Forms.Padding(14, 10, 14, 10); + statusPanel.Size = new System.Drawing.Size(450, 100); + statusPanel.TabIndex = 2; + + // + // statusHeader + // + statusHeader.AutoSize = true; + statusHeader.Dock = System.Windows.Forms.DockStyle.Top; + statusHeader.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Bold); + statusHeader.ForeColor = System.Drawing.Color.FromArgb(85, 85, 85); + statusHeader.Name = "statusHeader"; + statusHeader.Padding = new System.Windows.Forms.Padding(0, 0, 0, 4); + statusHeader.Size = new System.Drawing.Size(55, 19); + statusHeader.TabIndex = 0; + statusHeader.Text = "STATUS"; + + // + // statusTable + // + statusTable.AutoSize = true; + statusTable.ColumnCount = 2; + statusTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 80F)); + statusTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); + statusTable.Controls.Add(plateLabel, 0, 0); + statusTable.Controls.Add(plateValue, 1, 0); + statusTable.Controls.Add(elapsedLabel, 0, 1); + statusTable.Controls.Add(elapsedValue, 1, 1); + statusTable.Controls.Add(descriptionLabel, 0, 2); + statusTable.Controls.Add(descriptionValue, 1, 2); + statusTable.Dock = System.Windows.Forms.DockStyle.Top; + statusTable.Name = "statusTable"; + statusTable.RowCount = 3; + statusTable.RowStyles.Add(new System.Windows.Forms.RowStyle()); + statusTable.RowStyles.Add(new System.Windows.Forms.RowStyle()); + statusTable.RowStyles.Add(new System.Windows.Forms.RowStyle()); + statusTable.TabIndex = 1; + + // + // plateLabel + // + plateLabel.AutoSize = true; + plateLabel.Font = new System.Drawing.Font("Segoe UI", 8.25F, System.Drawing.FontStyle.Bold); + plateLabel.ForeColor = System.Drawing.Color.FromArgb(51, 51, 51); + plateLabel.Margin = new System.Windows.Forms.Padding(0, 3, 5, 3); + plateLabel.Name = "plateLabel"; + plateLabel.Text = "Plate:"; + + // + // plateValue + // + plateValue.AutoSize = true; + plateValue.Font = new System.Drawing.Font("Consolas", 8.25F); + plateValue.Margin = new System.Windows.Forms.Padding(0, 3, 0, 3); + plateValue.Name = "plateValue"; + plateValue.Text = "\u2014"; + + // + // elapsedLabel + // + elapsedLabel.AutoSize = true; + elapsedLabel.Font = new System.Drawing.Font("Segoe UI", 8.25F, System.Drawing.FontStyle.Bold); + elapsedLabel.ForeColor = System.Drawing.Color.FromArgb(51, 51, 51); + elapsedLabel.Margin = new System.Windows.Forms.Padding(0, 3, 5, 3); + elapsedLabel.Name = "elapsedLabel"; + elapsedLabel.Text = "Elapsed:"; + + // + // elapsedValue + // + elapsedValue.AutoSize = true; + elapsedValue.Font = new System.Drawing.Font("Consolas", 8.25F); + elapsedValue.Margin = new System.Windows.Forms.Padding(0, 3, 0, 3); + elapsedValue.Name = "elapsedValue"; + elapsedValue.Text = "0:00"; + + // + // descriptionLabel + // + descriptionLabel.AutoSize = true; + descriptionLabel.Font = new System.Drawing.Font("Segoe UI", 8.25F, System.Drawing.FontStyle.Bold); + descriptionLabel.ForeColor = System.Drawing.Color.FromArgb(51, 51, 51); + descriptionLabel.Margin = new System.Windows.Forms.Padding(0, 3, 5, 3); + descriptionLabel.Name = "descriptionLabel"; + descriptionLabel.Text = "Detail:"; + + // + // descriptionValue + // + descriptionValue.AutoSize = true; + descriptionValue.Font = new System.Drawing.Font("Segoe UI", 8.25F); + descriptionValue.Margin = new System.Windows.Forms.Padding(0, 3, 0, 3); + descriptionValue.Name = "descriptionValue"; + descriptionValue.Text = "\u2014"; + + // + // buttonPanel + // + buttonPanel.AutoSize = true; + buttonPanel.Controls.Add(stopButton); + buttonPanel.Controls.Add(acceptButton); + buttonPanel.Dock = System.Windows.Forms.DockStyle.Top; + buttonPanel.FlowDirection = System.Windows.Forms.FlowDirection.RightToLeft; + buttonPanel.Name = "buttonPanel"; + buttonPanel.Padding = new System.Windows.Forms.Padding(9, 6, 9, 6); + buttonPanel.Size = new System.Drawing.Size(450, 45); + buttonPanel.TabIndex = 3; + + // + // acceptButton + // + acceptButton.Enabled = false; + acceptButton.Font = new System.Drawing.Font("Segoe UI", 8.25F); + acceptButton.Margin = new System.Windows.Forms.Padding(6, 3, 0, 3); + acceptButton.Name = "acceptButton"; + acceptButton.Size = new System.Drawing.Size(93, 27); + acceptButton.TabIndex = 1; + acceptButton.Text = "Accept"; + acceptButton.UseVisualStyleBackColor = true; + acceptButton.Click += AcceptButton_Click; + + // + // stopButton + // + stopButton.Enabled = false; + stopButton.Font = new System.Drawing.Font("Segoe UI", 8.25F); + stopButton.Margin = new System.Windows.Forms.Padding(0, 3, 0, 3); + stopButton.Name = "stopButton"; + stopButton.Size = new System.Drawing.Size(93, 27); + stopButton.TabIndex = 0; + stopButton.Text = "Stop"; + stopButton.UseVisualStyleBackColor = true; + stopButton.Click += StopButton_Click; + + // + // NestProgressForm + // + AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); + AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + ClientSize = new System.Drawing.Size(450, 315); + Controls.Add(buttonPanel); + Controls.Add(statusPanel); + Controls.Add(resultsPanel); + Controls.Add(phaseStepper); + FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedToolWindow; + MaximizeBox = false; + MinimizeBox = false; + Name = "NestProgressForm"; + ShowInTaskbar = false; + StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; + Text = "Nesting Progress"; + resultsPanel.ResumeLayout(false); + resultsPanel.PerformLayout(); + resultsTable.ResumeLayout(false); + resultsTable.PerformLayout(); + densityPanel.ResumeLayout(false); + densityPanel.PerformLayout(); + statusPanel.ResumeLayout(false); + statusPanel.PerformLayout(); + statusTable.ResumeLayout(false); + statusTable.PerformLayout(); + buttonPanel.ResumeLayout(false); + ResumeLayout(false); + PerformLayout(); + } + + #endregion + + private Controls.PhaseStepperControl phaseStepper; + private System.Windows.Forms.Panel resultsPanel; + private System.Windows.Forms.Label resultsHeader; + private System.Windows.Forms.TableLayoutPanel resultsTable; + private System.Windows.Forms.Label partsLabel; + private System.Windows.Forms.Label partsValue; + private System.Windows.Forms.Label densityLabel; + private System.Windows.Forms.FlowLayoutPanel densityPanel; + private System.Windows.Forms.Label densityValue; + private Controls.DensityBar densityBar; + private System.Windows.Forms.Label nestedAreaLabel; + private System.Windows.Forms.Label nestedAreaValue; + private System.Windows.Forms.Panel statusPanel; + private System.Windows.Forms.Label statusHeader; + private System.Windows.Forms.TableLayoutPanel statusTable; + private System.Windows.Forms.Label plateLabel; + private System.Windows.Forms.Label plateValue; + private System.Windows.Forms.Label elapsedLabel; + private System.Windows.Forms.Label elapsedValue; + private System.Windows.Forms.Label descriptionLabel; + private System.Windows.Forms.Label descriptionValue; + private System.Windows.Forms.FlowLayoutPanel buttonPanel; + private System.Windows.Forms.Button acceptButton; + private System.Windows.Forms.Button stopButton; + } +} +``` + +- [ ] **Step 2: Build to verify** + +Run: `dotnet build OpenNest/OpenNest.csproj --no-restore -v q` +Expected: Build errors — `NestProgressForm.cs` still references removed fields (`phaseValue`, `remnantValue`, etc.). This is expected; Task 4 will fix it. + +- [ ] **Step 3: Commit** + +Do NOT commit yet — the code-behind (Task 4) must be updated first to compile. + +--- + +### Task 4: Rewrite NestProgressForm Code-Behind + +**Files:** +- Modify: `OpenNest/Forms/NestProgressForm.cs` (full rewrite) + +- [ ] **Step 1: Rewrite the form code-behind** + +Replace the entire file with the updated logic: phase stepper integration, color-coded flash & fade with per-label color tracking, Accept/Stop buttons, updated FormatPhase, and DensityBar updates. + +```csharp +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Drawing; +using System.Linq; +using System.Threading; +using System.Windows.Forms; + +namespace OpenNest.Forms +{ + public partial class NestProgressForm : Form + { + private static readonly Color DefaultFlashColor = Color.FromArgb(0, 160, 0); + private static readonly Color DensityLowColor = Color.FromArgb(200, 40, 40); + private static readonly Color DensityMidColor = Color.FromArgb(200, 160, 0); + private static readonly Color DensityHighColor = Color.FromArgb(0, 160, 0); + + private const int FadeSteps = 20; + private const int FadeIntervalMs = 50; + + private readonly CancellationTokenSource cts; + private readonly Stopwatch stopwatch = Stopwatch.StartNew(); + private readonly System.Windows.Forms.Timer elapsedTimer; + private readonly System.Windows.Forms.Timer fadeTimer; + private readonly Dictionary fadeCounters = new(); + private bool hasReceivedProgress; + + public bool Accepted { get; private set; } + + public NestProgressForm(CancellationTokenSource cts, bool showPlateRow = true) + { + this.cts = cts; + InitializeComponent(); + + if (!showPlateRow) + { + plateLabel.Visible = false; + plateValue.Visible = false; + } + + elapsedTimer = new System.Windows.Forms.Timer { Interval = 1000 }; + elapsedTimer.Tick += (s, e) => UpdateElapsed(); + elapsedTimer.Start(); + + fadeTimer = new System.Windows.Forms.Timer { Interval = FadeIntervalMs }; + fadeTimer.Tick += FadeTimer_Tick; + } + + public void UpdateProgress(NestProgress progress) + { + if (IsDisposed || !IsHandleCreated) + return; + + if (!hasReceivedProgress) + { + hasReceivedProgress = true; + acceptButton.Enabled = true; + stopButton.Enabled = true; + } + + phaseStepper.ActivePhase = progress.Phase; + + SetValueWithFlash(plateValue, progress.PlateNumber.ToString()); + SetValueWithFlash(partsValue, progress.BestPartCount.ToString()); + + var densityText = progress.BestDensity.ToString("P1"); + var densityFlashColor = GetDensityColor(progress.BestDensity); + SetValueWithFlash(densityValue, densityText, densityFlashColor); + densityBar.Value = progress.BestDensity; + + SetValueWithFlash(nestedAreaValue, + $"{progress.NestedWidth:F1} x {progress.NestedLength:F1} ({progress.NestedArea:F1} sq in)"); + + descriptionValue.Text = !string.IsNullOrEmpty(progress.Description) + ? progress.Description + : FormatPhase(progress.Phase); + } + + public void ShowCompleted() + { + if (IsDisposed || !IsHandleCreated) + return; + + stopwatch.Stop(); + elapsedTimer.Stop(); + UpdateElapsed(); + + phaseStepper.IsComplete = true; + descriptionValue.Text = "\u2014"; + + acceptButton.Visible = false; + stopButton.Text = "Close"; + stopButton.Enabled = true; + stopButton.Click -= StopButton_Click; + stopButton.Click += (s, e) => Close(); + } + + private void UpdateElapsed() + { + if (IsDisposed || !IsHandleCreated) + return; + + var elapsed = stopwatch.Elapsed; + elapsedValue.Text = elapsed.TotalHours >= 1 + ? elapsed.ToString(@"h\:mm\:ss") + : elapsed.ToString(@"m\:ss"); + } + + private void AcceptButton_Click(object sender, EventArgs e) + { + Accepted = true; + cts.Cancel(); + acceptButton.Enabled = false; + stopButton.Enabled = false; + acceptButton.Text = "Accepted"; + stopButton.Text = "Stopping..."; + } + + private void StopButton_Click(object sender, EventArgs e) + { + cts.Cancel(); + acceptButton.Enabled = false; + stopButton.Enabled = false; + stopButton.Text = "Stopping..."; + } + + protected override void OnFormClosing(FormClosingEventArgs e) + { + fadeTimer.Stop(); + fadeTimer.Dispose(); + elapsedTimer.Stop(); + elapsedTimer.Dispose(); + stopwatch.Stop(); + + if (!cts.IsCancellationRequested) + cts.Cancel(); + + base.OnFormClosing(e); + } + + private void SetValueWithFlash(Label label, string text, Color? flashColor = null) + { + if (label.Text == text) + return; + + var color = flashColor ?? DefaultFlashColor; + label.Text = text; + label.ForeColor = color; + fadeCounters[label] = (FadeSteps, color); + + if (!fadeTimer.Enabled) + fadeTimer.Start(); + } + + private void FadeTimer_Tick(object sender, EventArgs e) + { + if (IsDisposed || !IsHandleCreated) + { + fadeTimer.Stop(); + return; + } + + var defaultColor = SystemColors.ControlText; + var labels = fadeCounters.Keys.ToList(); + + foreach (var label in labels) + { + var (remaining, flashColor) = fadeCounters[label]; + remaining--; + + if (remaining <= 0) + { + label.ForeColor = defaultColor; + fadeCounters.Remove(label); + } + else + { + var ratio = (float)remaining / FadeSteps; + var r = (int)(defaultColor.R + (flashColor.R - defaultColor.R) * ratio); + var g = (int)(defaultColor.G + (flashColor.G - defaultColor.G) * ratio); + var b = (int)(defaultColor.B + (flashColor.B - defaultColor.B) * ratio); + label.ForeColor = Color.FromArgb(r, g, b); + fadeCounters[label] = (remaining, flashColor); + } + } + + if (fadeCounters.Count == 0) + fadeTimer.Stop(); + } + + private static Color GetDensityColor(double density) + { + if (density < 0.5) + return DensityLowColor; + if (density < 0.7) + return DensityMidColor; + return DensityHighColor; + } + + 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.Extents: return "Trying extents..."; + case NestPhase.Nfp: return "Trying NFP..."; + default: return phase.ToString(); + } + } + } +} +``` + +- [ ] **Step 2: Build to verify** + +Run: `dotnet build OpenNest/OpenNest.csproj --no-restore -v q` +Expected: 0 errors + +- [ ] **Step 3: Commit** + +```bash +git add OpenNest/Forms/NestProgressForm.cs OpenNest/Forms/NestProgressForm.Designer.cs +git commit -m "feat(ui): rewrite NestProgressForm with grouped panels, stepper, density bar, and Accept button" +``` + +--- + +### Task 5: Update Callers for Accept Support + +**Files:** +- Modify: `OpenNest/Forms/MainForm.cs:868` +- Modify: `OpenNest/Controls/PlateView.cs:933` + +- [ ] **Step 1: Update `RunAutoNest_Click` in MainForm.cs** + +At line 868, change the cancellation check to also honor `Accepted`: + +```csharp +// Before: +if (nestParts.Count > 0 && !token.IsCancellationRequested) +// After: +if (nestParts.Count > 0 && (!token.IsCancellationRequested || progressForm.Accepted)) +``` + +- [ ] **Step 2: Update `FillWithProgress` in PlateView.cs** + +At line 933, same change: + +```csharp +// Before: +if (parts.Count > 0 && !cts.IsCancellationRequested) +// After: +if (parts.Count > 0 && (!cts.IsCancellationRequested || progressForm.Accepted)) +``` + +- [ ] **Step 3: Build to verify** + +Run: `dotnet build OpenNest.sln --no-restore -v q` +Expected: 0 errors + +- [ ] **Step 4: Commit** + +```bash +git add OpenNest/Forms/MainForm.cs OpenNest/Controls/PlateView.cs +git commit -m "feat(ui): support Accept button in nesting callers" +``` + +--- + +### Task 6: Build Verification and Layout Tuning + +**Files:** None new — adjustments to existing files from Tasks 1-5 if needed. + +- [ ] **Step 1: Full solution build** + +Run: `dotnet build OpenNest.sln -v q` +Expected: 0 errors + +- [ ] **Step 2: Run existing tests** + +Run: `dotnet test OpenNest.Tests/OpenNest.Tests.csproj --no-build -v q` +Expected: All tests pass (no regressions — these tests don't touch the UI) + +- [ ] **Step 3: Visual review checklist** + +Launch the app and open or create a nest with drawings. Run a fill operation and verify: + +1. Phase stepper shows circles for all 6 phases — initially all hollow/pending +2. As the engine progresses, active phase circle is larger with glow, visited phases are filled blue +3. Results section has white background with "RESULTS" header, monospaced values +4. Status section has white background with "STATUS" header +5. Density bar shows next to percentage, gradient from orange to green +6. Values flash green (or red/yellow for low density) and fade back to black on change +7. Accept button stops engine and keeps result on plate +8. Stop button stops engine and discards result +9. Both buttons disabled until first progress update +10. After ShowCompleted(), both replaced by "Close" button + +- [ ] **Step 4: Final commit if any tuning was needed** + +```bash +git add -A +git commit -m "fix(ui): tune progress form layout spacing" +```