feat(ui): add PhaseStepperControl for nesting progress phases
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
146
OpenNest/Controls/PhaseStepperControl.cs
Normal file
146
OpenNest/Controls/PhaseStepperControl.cs
Normal file
@@ -0,0 +1,146 @@
|
||||
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<NestPhase> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user