using System;
using System.Globalization;
using System.Windows;
using System.Windows.Media;
namespace HC_APTBS.Views.Controls
{
///
/// Geometry-rendered circular angle gauge for the bench advance-monitoring display.
/// Shows a 360° dial with 5° tick steps, major labels every 45°, and up to three
/// directional thumb markers: a primary thumb (manual wheel or PSG), an optional
/// target thumb (lock-angle target, colour-coded for tolerance), and an optional
/// ghost thumb (secondary / de-emphasised context indicator).
///
public sealed class RadialAngleGauge : FrameworkElement
{
// ── Dependency properties ─────────────────────────────────────────────────
public static readonly DependencyProperty PrimaryAngleProperty =
DependencyProperty.Register(nameof(PrimaryAngle), typeof(double), typeof(RadialAngleGauge),
new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsRender));
public static readonly DependencyProperty TargetAngleProperty =
DependencyProperty.Register(nameof(TargetAngle), typeof(double?), typeof(RadialAngleGauge),
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsRender));
public static readonly DependencyProperty SecondaryAngleProperty =
DependencyProperty.Register(nameof(SecondaryAngle), typeof(double?), typeof(RadialAngleGauge),
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsRender));
public static readonly DependencyProperty PrimaryBrushProperty =
DependencyProperty.Register(nameof(PrimaryBrush), typeof(Brush), typeof(RadialAngleGauge),
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsRender));
public static readonly DependencyProperty TargetBrushProperty =
DependencyProperty.Register(nameof(TargetBrush), typeof(Brush), typeof(RadialAngleGauge),
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsRender));
public static readonly DependencyProperty IsRunningModeProperty =
DependencyProperty.Register(nameof(IsRunningMode), typeof(bool), typeof(RadialAngleGauge),
new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender));
// ── Property accessors ────────────────────────────────────────────────────
/// Primary thumb angle in degrees (0 = top, clockwise positive).
public double PrimaryAngle
{
get => (double)GetValue(PrimaryAngleProperty);
set => SetValue(PrimaryAngleProperty, value);
}
/// Target thumb angle in degrees, or null to hide it.
public double? TargetAngle
{
get => (double?)GetValue(TargetAngleProperty);
set => SetValue(TargetAngleProperty, value);
}
/// Ghost secondary thumb angle in degrees, or null to hide it.
public double? SecondaryAngle
{
get => (double?)GetValue(SecondaryAngleProperty);
set => SetValue(SecondaryAngleProperty, value);
}
/// Fill brush for the primary thumb. Falls back to the accent brush when null.
public Brush? PrimaryBrush
{
get => (Brush?)GetValue(PrimaryBrushProperty);
set => SetValue(PrimaryBrushProperty, value);
}
/// Fill brush for the target thumb (green = within tolerance, red = off-target).
public Brush? TargetBrush
{
get => (Brush?)GetValue(TargetBrushProperty);
set => SetValue(TargetBrushProperty, value);
}
/// When true the mode label switches to PSG context; adjusts visual emphasis.
public bool IsRunningMode
{
get => (bool)GetValue(IsRunningModeProperty);
set => SetValue(IsRunningModeProperty, value);
}
// ── Layout ────────────────────────────────────────────────────────────────
protected override Size MeasureOverride(Size availableSize)
{
double side = double.IsInfinity(availableSize.Width) ? 240.0 : availableSize.Width;
if (!double.IsInfinity(availableSize.Height))
side = Math.Min(side, availableSize.Height);
side = Math.Max(side, 120.0);
return new Size(side, side);
}
// ── Render ────────────────────────────────────────────────────────────────
protected override void OnRender(DrawingContext dc)
{
double w = ActualWidth;
double h = ActualHeight;
double side = Math.Min(w, h);
double cx = w / 2.0;
double cy = h / 2.0;
// Radii as fractions of half-side
double halfS = side / 2.0;
double rOuter = halfS * 0.88;
double rInner = halfS * 0.82;
double rTickOut = halfS * 0.86;
double rMinorIn = halfS * 0.80;
double rMajorIn = halfS * 0.74;
double rLabel = halfS * 0.68;
double rThumbTip = halfS * 0.88;
double rThumbBase= halfS * 0.72;
double thumbHalf = halfS * 0.06; // half-width of triangle base
// Brushes — use WPF-UI resources when available, safe hard-coded fallbacks otherwise
Brush ringBrush = GetThemeBrush("ControlStrokeColorDefaultBrush", new SolidColorBrush(Color.FromArgb(64, 200, 200, 200)));
Brush tickBrush = GetThemeBrush("TextFillColorSecondaryBrush", new SolidColorBrush(Color.FromArgb(120, 180, 180, 180)));
Brush majorBrush = GetThemeBrush("TextFillColorPrimaryBrush", Brushes.WhiteSmoke);
Brush accentBrush = GetThemeBrush("AccentFillColorPrimaryBrush", Brushes.DodgerBlue);
Brush primaryFill = PrimaryBrush ?? accentBrush;
Brush targetFill = TargetBrush ?? Brushes.White;
Brush ghostFill = new SolidColorBrush(Color.FromArgb(80, 200, 200, 200));
var ringPen = new Pen(ringBrush, 1.0);
var minorPen = new Pen(tickBrush, 1.0);
var majorPen = new Pen(majorBrush, 2.0);
// Background ring (outer circle)
dc.DrawEllipse(null, ringPen, new Point(cx, cy), rOuter, rOuter);
dc.DrawEllipse(null, new Pen(ringBrush, 0.5), new Point(cx, cy), rInner, rInner);
// Tick marks and labels
var labelTypeface = new Typeface("Segoe UI");
double dpi = VisualTreeHelper.GetDpi(this).PixelsPerDip;
for (int deg = 0; deg < 360; deg += 5)
{
bool isMajor = (deg % 45 == 0);
double rad = deg * Math.PI / 180.0;
double sinA = Math.Sin(rad);
double cosA = Math.Cos(rad);
double oR = rTickOut;
double iR = isMajor ? rMajorIn : rMinorIn;
var p1 = new Point(cx + oR * sinA, cy - oR * cosA);
var p2 = new Point(cx + iR * sinA, cy - iR * cosA);
dc.DrawLine(isMajor ? majorPen : minorPen, p1, p2);
if (isMajor)
{
string label = deg.ToString(CultureInfo.InvariantCulture);
var ft = new FormattedText(label, CultureInfo.CurrentCulture, FlowDirection.LeftToRight,
labelTypeface, 10, majorBrush, dpi);
var labelPt = new Point(
cx + rLabel * sinA - ft.Width / 2,
cy - rLabel * cosA - ft.Height / 2);
dc.DrawText(ft, labelPt);
}
}
// Ghost / secondary thumb
if (SecondaryAngle.HasValue)
DrawThumb(dc, cx, cy, SecondaryAngle.Value, rThumbTip, rThumbBase, thumbHalf, ghostFill, null);
// Target thumb (outline + thin fill)
if (TargetAngle.HasValue)
{
var targetOutline = new Pen(targetFill, 1.5);
DrawThumb(dc, cx, cy, TargetAngle.Value, rThumbTip + halfS * 0.03, rThumbBase - halfS * 0.03, thumbHalf * 1.3, null, targetOutline);
}
// Primary thumb (filled)
DrawThumb(dc, cx, cy, PrimaryAngle, rThumbTip, rThumbBase, thumbHalf, primaryFill, null);
// Centre readout
string centre = $"{PrimaryAngle:F1}°";
var cft = new FormattedText(centre, CultureInfo.CurrentCulture, FlowDirection.LeftToRight,
new Typeface("Consolas"), 14, primaryFill, dpi);
dc.DrawText(cft, new Point(cx - cft.Width / 2, cy - cft.Height / 2));
// Mode label (tiny)
string modeLabel = IsRunningMode ? "PSG" : "MAN";
var mft = new FormattedText(modeLabel, CultureInfo.CurrentCulture, FlowDirection.LeftToRight,
new Typeface("Segoe UI"), 9, tickBrush, dpi);
dc.DrawText(mft, new Point(cx - mft.Width / 2, cy + 12));
}
// ── Helpers ───────────────────────────────────────────────────────────────
/// Draws an inward-pointing triangle thumb at the given dial angle.
private static void DrawThumb(
DrawingContext dc, double cx, double cy,
double angleDeg, double tipR, double baseR, double halfWidth,
Brush? fill, Pen? outline)
{
double rad = angleDeg * Math.PI / 180.0;
double sinA = Math.Sin(rad);
double cosA = Math.Cos(rad);
double perpSin = Math.Sin(rad + Math.PI / 2.0);
double perpCos = Math.Cos(rad + Math.PI / 2.0);
var tip = new Point(cx + tipR * sinA, cy - tipR * cosA);
var left = new Point(cx + baseR * sinA + halfWidth * perpSin,
cy - baseR * cosA - halfWidth * perpCos);
var right = new Point(cx + baseR * sinA - halfWidth * perpSin,
cy - baseR * cosA + halfWidth * perpCos);
var geo = new StreamGeometry();
using (var ctx = geo.Open())
{
ctx.BeginFigure(tip, true, true);
ctx.LineTo(left, true, false);
ctx.LineTo(right, true, false);
}
geo.Freeze();
dc.DrawGeometry(fill, outline, geo);
}
/// Resolves a WPF-UI theme brush by key with a safe fallback.
private Brush GetThemeBrush(string key, Brush fallback)
=> TryFindResource(key) as Brush ?? fallback;
}
}