feat: redesign Bench page with Fluent card layout and radial advance monitor

Three-column layout replacing the old HMI grid:
- BenchRpmCommandCard: inline numeric input, 2×4 preset grid, Start/Stop
- BenchActuatorsCard: direction toggle, oil pump, temperature PID, misc relays
  with FluentStateToggle showing checked state via AccentFillColor
- BenchLiveDataCard: 2×5 KPI tiles (RPM, P1, P2, Q-Delivery, Q-Over, temps)
- BenchChartsCard: 2×2 compact chart grid (Delivery, Over, P1, P2)
- AdvanceMonitorCard: RadialAngleGauge custom FrameworkElement + PSG/INJ readouts,
  Δ° lock offset input, Zero PSG / Zero INJ buttons

Supporting changes:
- AngleDisplayViewModel: promote _currentManualDegrees, _isLockSet to
  [ObservableProperty]; add PsgRelativeDegrees, InjEncoderDegreesValue,
  TargetLockAngle, IsRunningMode (29/31 hysteresis); computed PrimaryGaugeAngle,
  TargetAngleForGauge, SecondaryGaugeAngle
- BenchControlViewModel: add IsDirectionLeft computed property,
  SetDirectionRightCommand, SetDirectionLeftCommand, ApplyRpmCommand
- FlowmeterChartView: add IsCompact DP (false default) for 90px compact height
- Styles.xaml: add IsChecked trigger to FluentStateToggle (accent fill + white text)
- Strings.en/es.xaml: add all new card and actuator string keys

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-20 17:45:59 +02:00
parent 70be693116
commit 69bfda54e1
19 changed files with 1361 additions and 59 deletions

View File

@@ -240,6 +240,52 @@
<sys:String x:Key="Angle.SetPsgZero">Set PSG zero reference</sys:String> <sys:String x:Key="Angle.SetPsgZero">Set PSG zero reference</sys:String>
<sys:String x:Key="Angle.SetInjZero">Set INJ zero reference</sys:String> <sys:String x:Key="Angle.SetInjZero">Set INJ zero reference</sys:String>
<!-- ── Bench page — Fluent redesign cards ───────────────────────────── -->
<sys:String x:Key="Bench.RpmCommand.Title">RPM Command</sys:String>
<sys:String x:Key="Bench.RpmCommand.Actual">Actual</sys:String>
<sys:String x:Key="Bench.RpmCommand.Target">Target</sys:String>
<sys:String x:Key="Bench.RpmCommand.Apply">Apply</sys:String>
<sys:String x:Key="Bench.Actuators.Title">Actuators &amp; Relays</sys:String>
<sys:String x:Key="Bench.Actuators.Direction">Direction</sys:String>
<sys:String x:Key="Bench.Actuators.DirRight">Right</sys:String>
<sys:String x:Key="Bench.Actuators.DirLeft">Left</sys:String>
<sys:String x:Key="Bench.Actuators.OilPump">Oil Pump</sys:String>
<sys:String x:Key="Bench.Actuators.Counter">Counter</sys:String>
<sys:String x:Key="Bench.Actuators.Set">Set</sys:String>
<sys:String x:Key="Bench.Actuators.Temperature">Temperature</sys:String>
<sys:String x:Key="Bench.Actuators.Setpoint">Setpoint °C</sys:String>
<sys:String x:Key="Bench.Actuators.Tolerance">Tolerance ±°C</sys:String>
<sys:String x:Key="Bench.Actuators.Heater">Heater</sys:String>
<sys:String x:Key="Bench.Actuators.DepositCooler">Dep. Cool.</sys:String>
<sys:String x:Key="Bench.Actuators.TinCooler">T-In Cool.</sys:String>
<sys:String x:Key="Bench.Actuators.Relays">Misc Relays</sys:String>
<sys:String x:Key="Bench.Actuators.Electronic">Electronic</sys:String>
<sys:String x:Key="Bench.Actuators.Flasher">Flasher</sys:String>
<sys:String x:Key="Bench.Actuators.Pulse4">Pulse 4</sys:String>
<sys:String x:Key="Bench.LiveData.Title">Live Data</sys:String>
<sys:String x:Key="Bench.Charts.Title">Live Charts</sys:String>
<sys:String x:Key="Bench.Advance.Title">Advance Monitor</sys:String>
<sys:String x:Key="Bench.Advance.Delta">Lock Offset Δ°</sys:String>
<sys:String x:Key="Bench.Advance.ZeroPsg">Zero PSG</sys:String>
<sys:String x:Key="Bench.Advance.ZeroInj">Zero INJ</sys:String>
<sys:String x:Key="Bench.Advance.Lock">Lock</sys:String>
<sys:String x:Key="Bench.Kpi.Rpm">Bench RPM</sys:String>
<sys:String x:Key="Bench.Kpi.P1">Pressure P1</sys:String>
<sys:String x:Key="Bench.Kpi.P2">Pressure P2</sys:String>
<sys:String x:Key="Bench.Kpi.QDelivery">Q Delivery</sys:String>
<sys:String x:Key="Bench.Kpi.QOver">Q Over</sys:String>
<sys:String x:Key="Bench.Kpi.TIn">T-In</sys:String>
<sys:String x:Key="Bench.Kpi.TOut">T-Out</sys:String>
<sys:String x:Key="Bench.Kpi.T4">T4</sys:String>
<sys:String x:Key="Bench.Kpi.TTank">T-Tank</sys:String>
<sys:String x:Key="Bench.Kpi.BenchTemp">Bench Temp</sys:String>
<sys:String x:Key="Bench.Kpi.Unit.Bar">bar</sys:String>
<sys:String x:Key="Bench.Kpi.Unit.CcS">cc/s</sys:String>
<sys:String x:Key="Bench.Chart.Delivery">Q Delivery</sys:String>
<sys:String x:Key="Bench.Chart.Over">Q Over</sys:String>
<sys:String x:Key="Bench.Chart.P1">Pressure P1</sys:String>
<sys:String x:Key="Bench.Chart.P2">Pressure P2</sys:String>
<!-- ── Test panel ───────────────────────────────────────────────────── --> <!-- ── Test panel ───────────────────────────────────────────────────── -->
<sys:String x:Key="Test.StartTest">▶ START TEST</sys:String> <sys:String x:Key="Test.StartTest">▶ START TEST</sys:String>
<sys:String x:Key="Test.Stop">■ STOP</sys:String> <sys:String x:Key="Test.Stop">■ STOP</sys:String>

View File

@@ -240,6 +240,52 @@
<sys:String x:Key="Angle.SetPsgZero">Fijar referencia cero PSG</sys:String> <sys:String x:Key="Angle.SetPsgZero">Fijar referencia cero PSG</sys:String>
<sys:String x:Key="Angle.SetInjZero">Fijar referencia cero INJ</sys:String> <sys:String x:Key="Angle.SetInjZero">Fijar referencia cero INJ</sys:String>
<!-- ── Bench page — Fluent redesign cards ───────────────────────────── -->
<sys:String x:Key="Bench.RpmCommand.Title">Mando RPM</sys:String>
<sys:String x:Key="Bench.RpmCommand.Actual">Actual</sys:String>
<sys:String x:Key="Bench.RpmCommand.Target">Objetivo</sys:String>
<sys:String x:Key="Bench.RpmCommand.Apply">Aplicar</sys:String>
<sys:String x:Key="Bench.Actuators.Title">Actuadores y Relés</sys:String>
<sys:String x:Key="Bench.Actuators.Direction">Dirección</sys:String>
<sys:String x:Key="Bench.Actuators.DirRight">Derecha</sys:String>
<sys:String x:Key="Bench.Actuators.DirLeft">Izquierda</sys:String>
<sys:String x:Key="Bench.Actuators.OilPump">Bomba de Aceite</sys:String>
<sys:String x:Key="Bench.Actuators.Counter">Contador</sys:String>
<sys:String x:Key="Bench.Actuators.Set">Fijar</sys:String>
<sys:String x:Key="Bench.Actuators.Temperature">Temperatura</sys:String>
<sys:String x:Key="Bench.Actuators.Setpoint">Consigna °C</sys:String>
<sys:String x:Key="Bench.Actuators.Tolerance">Tolerancia ±°C</sys:String>
<sys:String x:Key="Bench.Actuators.Heater">Calefactor</sys:String>
<sys:String x:Key="Bench.Actuators.DepositCooler">Refr. Dep.</sys:String>
<sys:String x:Key="Bench.Actuators.TinCooler">Refr. T-In</sys:String>
<sys:String x:Key="Bench.Actuators.Relays">Relés Misc.</sys:String>
<sys:String x:Key="Bench.Actuators.Electronic">Electrónico</sys:String>
<sys:String x:Key="Bench.Actuators.Flasher">Flasher</sys:String>
<sys:String x:Key="Bench.Actuators.Pulse4">Pulso 4</sys:String>
<sys:String x:Key="Bench.LiveData.Title">Datos en Vivo</sys:String>
<sys:String x:Key="Bench.Charts.Title">Gráficos</sys:String>
<sys:String x:Key="Bench.Advance.Title">Monitor de Avance</sys:String>
<sys:String x:Key="Bench.Advance.Delta">Offset Bloqueo Δ°</sys:String>
<sys:String x:Key="Bench.Advance.ZeroPsg">Cero PSG</sys:String>
<sys:String x:Key="Bench.Advance.ZeroInj">Cero INJ</sys:String>
<sys:String x:Key="Bench.Advance.Lock">Bloqueo</sys:String>
<sys:String x:Key="Bench.Kpi.Rpm">RPM Banco</sys:String>
<sys:String x:Key="Bench.Kpi.P1">Presión P1</sys:String>
<sys:String x:Key="Bench.Kpi.P2">Presión P2</sys:String>
<sys:String x:Key="Bench.Kpi.QDelivery">Q Entrega</sys:String>
<sys:String x:Key="Bench.Kpi.QOver">Q Derrame</sys:String>
<sys:String x:Key="Bench.Kpi.TIn">T-Ent.</sys:String>
<sys:String x:Key="Bench.Kpi.TOut">T-Sal.</sys:String>
<sys:String x:Key="Bench.Kpi.T4">T4</sys:String>
<sys:String x:Key="Bench.Kpi.TTank">T-Tanque</sys:String>
<sys:String x:Key="Bench.Kpi.BenchTemp">T. Banco</sys:String>
<sys:String x:Key="Bench.Kpi.Unit.Bar">bar</sys:String>
<sys:String x:Key="Bench.Kpi.Unit.CcS">cc/iny.</sys:String>
<sys:String x:Key="Bench.Chart.Delivery">Q Entrega</sys:String>
<sys:String x:Key="Bench.Chart.Over">Q Derrame</sys:String>
<sys:String x:Key="Bench.Chart.P1">Presión P1</sys:String>
<sys:String x:Key="Bench.Chart.P2">Presión P2</sys:String>
<!-- ── Test panel ───────────────────────────────────────────────────── --> <!-- ── Test panel ───────────────────────────────────────────────────── -->
<sys:String x:Key="Test.StartTest">▶ INICIAR TEST</sys:String> <sys:String x:Key="Test.StartTest">▶ INICIAR TEST</sys:String>
<sys:String x:Key="Test.Stop">■ PARAR</sys:String> <sys:String x:Key="Test.Stop">■ PARAR</sys:String>

View File

@@ -93,6 +93,11 @@
IsHitTestVisible="False"/> IsHitTestVisible="False"/>
</Grid> </Grid>
<ControlTemplate.Triggers> <ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="Root" Property="Background" Value="{DynamicResource AccentFillColorDefaultBrush}"/>
<Setter TargetName="Root" Property="BorderBrush" Value="{DynamicResource AccentFillColorDefaultBrush}"/>
<Setter Property="Foreground" Value="{DynamicResource TextOnAccentFillColorPrimaryBrush}"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True"> <Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="HoverOverlay" Property="Background" Value="#18000000"/> <Setter TargetName="HoverOverlay" Property="Background" Value="#18000000"/>
</Trigger> </Trigger>

View File

@@ -31,10 +31,15 @@ namespace HC_APTBS.ViewModels
private double _zeroPsgEncoder; private double _zeroPsgEncoder;
private double _zeroInjDegrees; private double _zeroInjDegrees;
private double _injEncoderDegrees; private double _injEncoderDegrees;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(PrimaryGaugeAngle))]
[NotifyPropertyChangedFor(nameof(SecondaryGaugeAngle))]
private double _currentManualDegrees; private double _currentManualDegrees;
private double _lockAngleValue; private double _lockAngleValue;
private bool _isLockSet; [ObservableProperty]
private bool _isManualVisible; [NotifyPropertyChangedFor(nameof(TargetAngleForGauge))]
private bool _isLockSet;
private bool _isManualVisible;
// ── Observable properties (bound by View) ───────────────────────────────── // ── Observable properties (bound by View) ─────────────────────────────────
@@ -68,6 +73,27 @@ namespace HC_APTBS.ViewModels
/// <summary>Foreground brush for the lock angle display (Green/Red/White).</summary> /// <summary>Foreground brush for the lock angle display (Green/Red/White).</summary>
[ObservableProperty] private Brush _lockAngleForeground = Brushes.White; [ObservableProperty] private Brush _lockAngleForeground = Brushes.White;
// ── Numeric doubles for radial advance-monitor control ────────────────────
/// <summary>PSG relative angle as a double for gauge binding.</summary>
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(PrimaryGaugeAngle))]
private double _psgRelativeDegrees;
/// <summary>INJ encoder absolute degrees (0360) as a double.</summary>
[ObservableProperty] private double _injEncoderDegreesValue;
/// <summary>Lock angle target (0360 degrees) as a double for gauge binding.</summary>
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(TargetAngleForGauge))]
private double _targetLockAngle;
/// <summary>True when bench RPM ≥ 31, hysteresis-cleared below 29.</summary>
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(PrimaryGaugeAngle))]
[NotifyPropertyChangedFor(nameof(SecondaryGaugeAngle))]
private bool _isRunningMode;
// ── Constructor ─────────────────────────────────────────────────────────── // ── Constructor ───────────────────────────────────────────────────────────
/// <summary> /// <summary>
@@ -78,6 +104,17 @@ namespace HC_APTBS.ViewModels
_encoderResolution = configService.Settings.EncoderResolution; _encoderResolution = configService.Settings.EncoderResolution;
} }
// ── Computed for radial gauge ─────────────────────────────────────────────
/// <summary>Primary thumb angle: manual-wheel angle in hand-mode, PSG relative angle when running.</summary>
public double PrimaryGaugeAngle => IsRunningMode ? PsgRelativeDegrees : CurrentManualDegrees;
/// <summary>Target thumb angle for the radial gauge, or null when no lock has been set yet.</summary>
public double? TargetAngleForGauge => IsLockSet ? TargetLockAngle : (double?)null;
/// <summary>Ghost secondary thumb angle: manual-wheel position shown when running, null in hand-mode (primary already shows it).</summary>
public double? SecondaryGaugeAngle => IsRunningMode ? CurrentManualDegrees : (double?)null;
// ── Public update (called from MainViewModel.OnRefreshTick) ─────────────── // ── Public update (called from MainViewModel.OnRefreshTick) ───────────────
/// <summary> /// <summary>
@@ -98,6 +135,11 @@ namespace HC_APTBS.ViewModels
_currentRpm = rpm; _currentRpm = rpm;
_isDirectionRight = isDirectionRight; _isDirectionRight = isDirectionRight;
if (!IsRunningMode && rpm >= 31.0)
IsRunningMode = true;
else if (IsRunningMode && rpm < 29.0)
IsRunningMode = false;
RecalculatePsg(); RecalculatePsg();
RecalculateInj(); RecalculateInj();
RecalculateManual(); RecalculateManual();
@@ -150,7 +192,8 @@ namespace HC_APTBS.ViewModels
if (double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out double delta)) if (double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out double delta))
{ {
_lockAngleValue = CalculateLockAngle(delta); _lockAngleValue = CalculateLockAngle(delta);
_isLockSet = true; TargetLockAngle = _lockAngleValue;
IsLockSet = true;
LockAngleDisplay = FormatAngle(_lockAngleValue); LockAngleDisplay = FormatAngle(_lockAngleValue);
} }
@@ -183,8 +226,9 @@ namespace HC_APTBS.ViewModels
if (relDeg > 180) relDeg = 360 - relDeg; if (relDeg > 180) relDeg = 360 - relDeg;
relDeg *= sign; relDeg *= sign;
PsgEncoderAngle = FormatAngle(rawDeg); PsgRelativeDegrees = relDeg;
PsgRelativeAngle = FormatAngle(relDeg); PsgEncoderAngle = FormatAngle(rawDeg);
PsgRelativeAngle = FormatAngle(relDeg);
} }
private void RecalculateInj() private void RecalculateInj()
@@ -202,7 +246,8 @@ namespace HC_APTBS.ViewModels
double sign = DirectionSign; double sign = DirectionSign;
// INJ encoder → degrees (full 0360, NO 180 normalization). // INJ encoder → degrees (full 0360, NO 180 normalization).
_injEncoderDegrees = _injRaw * (360.0 / _encoderResolution); _injEncoderDegrees = _injRaw * (360.0 / _encoderResolution);
InjEncoderDegreesValue = _injEncoderDegrees;
// Relative angle from zero reference. // Relative angle from zero reference.
double relDeg = (_injEncoderDegrees - _zeroInjDegrees) * sign; double relDeg = (_injEncoderDegrees - _zeroInjDegrees) * sign;
@@ -216,13 +261,14 @@ namespace HC_APTBS.ViewModels
CultureInfo.InvariantCulture, out double delta)) CultureInfo.InvariantCulture, out double delta))
{ {
_lockAngleValue = CalculateLockAngle(delta); _lockAngleValue = CalculateLockAngle(delta);
TargetLockAngle = _lockAngleValue;
LockAngleDisplay = FormatAngle(_lockAngleValue); LockAngleDisplay = FormatAngle(_lockAngleValue);
} }
} }
private void RecalculateManual() private void RecalculateManual()
{ {
_currentManualDegrees = _manualRaw * (360.0 / _encoderResolution); CurrentManualDegrees = _manualRaw * (360.0 / _encoderResolution);
if (_currentRpm < 30) if (_currentRpm < 30)
{ {

View File

@@ -22,7 +22,12 @@ namespace HC_APTBS.ViewModels
// ── Direction ───────────────────────────────────────────────────────────── // ── Direction ─────────────────────────────────────────────────────────────
/// <summary>True when the bench rotates clockwise (right). False for counter-clockwise (left).</summary> /// <summary>True when the bench rotates clockwise (right). False for counter-clockwise (left).</summary>
[ObservableProperty] private bool _isDirectionRight = true; [ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsDirectionLeft))]
private bool _isDirectionRight = true;
/// <summary>True when the bench rotates counter-clockwise (left). Computed inverse of <see cref="IsDirectionRight"/>.</summary>
public bool IsDirectionLeft => !IsDirectionRight;
// ── Oil pump ────────────────────────────────────────────────────────────── // ── Oil pump ──────────────────────────────────────────────────────────────
@@ -85,6 +90,14 @@ namespace HC_APTBS.ViewModels
_bench.SetRelay(RelayNames.DirectionLeft, !value); _bench.SetRelay(RelayNames.DirectionLeft, !value);
} }
/// <summary>Sets the bench rotation direction to clockwise (right).</summary>
[RelayCommand]
private void SetDirectionRight() => IsDirectionRight = true;
/// <summary>Sets the bench rotation direction to counter-clockwise (left).</summary>
[RelayCommand]
private void SetDirectionLeft() => IsDirectionRight = false;
// ── Oil pump toggle ─────────────────────────────────────────────────────── // ── Oil pump toggle ───────────────────────────────────────────────────────
partial void OnIsOilPumpOnChanged(bool value) partial void OnIsOilPumpOnChanged(bool value)
@@ -167,6 +180,13 @@ namespace HC_APTBS.ViewModels
IsBenchRunning = false; IsBenchRunning = false;
} }
/// <summary>
/// Applies the RPM value from <see cref="RpmInputText"/> and starts the bench.
/// Bound to the inline Apply button in <c>BenchRpmCommandCard</c>.
/// </summary>
[RelayCommand]
private void ApplyRpm() => StartBench();
/// <summary> /// <summary>
/// Quick-select button handler: sets the RPM input and starts the bench. /// Quick-select button handler: sets the RPM input and starts the bench.
/// </summary> /// </summary>

View File

@@ -0,0 +1,228 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Media;
namespace HC_APTBS.Views.Controls
{
/// <summary>
/// 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).
/// </summary>
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 ────────────────────────────────────────────────────
/// <summary>Primary thumb angle in degrees (0 = top, clockwise positive).</summary>
public double PrimaryAngle
{
get => (double)GetValue(PrimaryAngleProperty);
set => SetValue(PrimaryAngleProperty, value);
}
/// <summary>Target thumb angle in degrees, or null to hide it.</summary>
public double? TargetAngle
{
get => (double?)GetValue(TargetAngleProperty);
set => SetValue(TargetAngleProperty, value);
}
/// <summary>Ghost secondary thumb angle in degrees, or null to hide it.</summary>
public double? SecondaryAngle
{
get => (double?)GetValue(SecondaryAngleProperty);
set => SetValue(SecondaryAngleProperty, value);
}
/// <summary>Fill brush for the primary thumb. Falls back to the accent brush when null.</summary>
public Brush? PrimaryBrush
{
get => (Brush?)GetValue(PrimaryBrushProperty);
set => SetValue(PrimaryBrushProperty, value);
}
/// <summary>Fill brush for the target thumb (green = within tolerance, red = off-target).</summary>
public Brush? TargetBrush
{
get => (Brush?)GetValue(TargetBrushProperty);
set => SetValue(TargetBrushProperty, value);
}
/// <summary>When true the mode label switches to PSG context; adjusts visual emphasis.</summary>
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 ───────────────────────────────────────────────────────────────
/// <summary>Draws an inward-pointing triangle thumb at the given dial angle.</summary>
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);
}
/// <summary>Resolves a WPF-UI theme brush by key with a safe fallback.</summary>
private Brush GetThemeBrush(string key, Brush fallback)
=> TryFindResource(key) as Brush ?? fallback;
}
}

View File

@@ -5,62 +5,59 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:uc="clr-namespace:HC_APTBS.Views.UserControls" xmlns:uc="clr-namespace:HC_APTBS.Views.UserControls"
mc:Ignorable="d" mc:Ignorable="d"
d:DesignHeight="900" d:DesignWidth="1280"> d:DesignHeight="900" d:DesignWidth="1400">
<!-- <!--
Bench page — HMI-style manual hardware operation. Bench page — Fluent card redesign.
DataContext = BenchPageViewModel. DataContext = BenchPageViewModel.
Three zones: Row 0: InterlockBannerView (Auto).
A. Live readings (LCD panel + encoder angles) Row 1: three-column card layout.
B. Live plots (Q flows + pressure traces) Col 0 (1* MinWidth=320): BenchRpmCommandCard (Auto) + BenchActuatorsCard (*).
C. Controls (drive, temperature, relay bank) Col 1 (1.8* MinWidth=520): BenchLiveDataCard (Auto) + BenchChartsCard (*).
Interlock banner spans the page above zone contents when triggered. Col 2 (1* MinWidth=300): AdvanceMonitorCard (*).
--> -->
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled"> <Grid Margin="12">
<Grid Margin="6"> <Grid.RowDefinitions>
<Grid.RowDefinitions> <RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/> <!-- interlock banner --> <RowDefinition Height="*"/>
<RowDefinition Height="*"/> <!-- main content --> </Grid.RowDefinitions>
</Grid.RowDefinitions>
<!-- Interlock banner: hidden unless InterlockBannerViewModel raises a condition --> <!-- Interlock banner hidden unless an interlock condition is active -->
<uc:InterlockBannerView Grid.Row="0" DataContext="{Binding Interlock}"/> <uc:InterlockBannerView Grid.Row="0" DataContext="{Binding Interlock}"
Margin="0,0,0,8"/>
<!-- Main content: 3 columns --> <!-- Three-column body -->
<Grid Grid.Row="1"> <Grid Grid.Row="1">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="400"/> <!-- Zone A: readings + angles --> <ColumnDefinition Width="1*" MinWidth="320"/>
<ColumnDefinition Width="*"/> <!-- Zone B: live plots --> <ColumnDefinition Width="8"/>
<ColumnDefinition Width="210"/> <!-- Zone C: controls --> <ColumnDefinition Width="1.8*" MinWidth="520"/>
</Grid.ColumnDefinitions> <ColumnDefinition Width="8"/>
<ColumnDefinition Width="1*" MinWidth="300"/>
<!-- ── Zone A: readings + encoder angles ──────────────────── --> </Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" Margin="0,0,6,0">
<uc:BenchReadingsView/>
<uc:AngleDisplayView DataContext="{Binding AngleDisplay}"
Margin="0,6,0,0"/>
</StackPanel>
<!-- ── Zone B: live plots (flows + pressure) ──────────────── -->
<StackPanel Grid.Column="1" Margin="0,0,6,0">
<uc:FlowmeterChartView DataContext="{Binding FlowmeterChart.Delivery}"/>
<uc:FlowmeterChartView DataContext="{Binding FlowmeterChart.Over}"
Margin="0,4,0,0"/>
<uc:FlowmeterChartView DataContext="{Binding PressureTrace.P1}"
Margin="0,4,0,0"/>
<uc:FlowmeterChartView DataContext="{Binding PressureTrace.P2}"
Margin="0,4,0,0"/>
</StackPanel>
<!-- ── Zone C: stacked control panels ─────────────────────── -->
<StackPanel Grid.Column="2">
<uc:BenchDriveControlView/>
<uc:TemperatureControlView DataContext="{Binding TempControl}"
Margin="0,10,0,0"/>
<uc:RelayBankView DataContext="{Binding RelayBank}"
Margin="0,10,0,0"/>
</StackPanel>
<!-- ── Col 0: RPM command (top) + Actuators (fill) ──────────── -->
<Grid Grid.Column="0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<uc:BenchRpmCommandCard Grid.Row="0"/>
<uc:BenchActuatorsCard Grid.Row="1" Margin="0,8,0,0"/>
</Grid> </Grid>
<!-- ── Col 2: Live data (top) + Charts (fill) ───────────────── -->
<Grid Grid.Column="2">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<uc:BenchLiveDataCard Grid.Row="0"/>
<uc:BenchChartsCard Grid.Row="1" Margin="0,8,0,0"/>
</Grid>
<!-- ── Col 4: Advance monitor (fills column) ────────────────── -->
<uc:AdvanceMonitorCard Grid.Column="4"/>
</Grid> </Grid>
</ScrollViewer> </Grid>
</UserControl> </UserControl>

View File

@@ -0,0 +1,125 @@
<UserControl x:Class="HC_APTBS.Views.UserControls.AdvanceMonitorCard"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:ctrl="clr-namespace:HC_APTBS.Views.Controls"
mc:Ignorable="d"
d:DesignHeight="560" d:DesignWidth="320">
<!--
Advance Monitor card — radial angle gauge, PSG/INJ readouts,
Δ° offset input, Zero PSG / Zero INJ buttons.
DataContext = BenchPageViewModel; all bindings go through AngleDisplay.*.
-->
<Border Style="{StaticResource PumpCard}">
<DockPanel LastChildFill="True">
<!-- ── Card header ─────────────────────────────────────────── -->
<DockPanel DockPanel.Dock="Top" Margin="0,0,0,10">
<ui:SymbolIcon DockPanel.Dock="Left" Symbol="CompassNorthwest24" FontSize="16"
Foreground="{DynamicResource AccentTextFillColorPrimaryBrush}"
Margin="0,0,8,0" VerticalAlignment="Center"/>
<TextBlock Text="{DynamicResource Bench.Advance.Title}"
Style="{StaticResource PumpCardHeader}" Margin="0"/>
</DockPanel>
<!-- ── Zero buttons (docked bottom first) ─────────────────── -->
<UniformGrid DockPanel.Dock="Bottom" Rows="1" Columns="2" Margin="0,10,0,0">
<ui:Button Margin="0,0,4,0" Height="40"
Content="{DynamicResource Bench.Advance.ZeroPsg}"
Command="{Binding AngleDisplay.SetPsgZeroCommand}"
Appearance="Secondary"/>
<ui:Button Margin="4,0,0,0" Height="40"
Content="{DynamicResource Bench.Advance.ZeroInj}"
Command="{Binding AngleDisplay.SetInjZeroCommand}"
Appearance="Secondary"/>
</UniformGrid>
<!-- ── Δ° offset row (docked bottom) ──────────────────────── -->
<Grid DockPanel.Dock="Bottom" Margin="0,10,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="90"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="{DynamicResource Bench.Advance.Delta}"
FontSize="13" FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
VerticalAlignment="Center" Margin="0,0,8,0"/>
<TextBox Grid.Column="1"
Text="{Binding AngleDisplay.LockAngleDeltaInput,
UpdateSourceTrigger=PropertyChanged}"
FontFamily="Consolas" FontSize="16" FontWeight="SemiBold"
Height="36" VerticalContentAlignment="Center"
HorizontalContentAlignment="Right" Padding="8,0"/>
<Border Grid.Column="3"
Background="{DynamicResource ControlFillColorSecondaryBrush}"
BorderBrush="{DynamicResource ControlStrokeColorDefaultBrush}"
BorderThickness="1" CornerRadius="6" Padding="8,4">
<TextBlock Text="{Binding AngleDisplay.LockAngleDisplay}"
Foreground="{Binding AngleDisplay.LockAngleForeground}"
FontFamily="Consolas" FontSize="16" FontWeight="SemiBold"
VerticalAlignment="Center" HorizontalAlignment="Center"/>
</Border>
</Grid>
<!-- ── PSG / INJ mini readouts (docked bottom) ────────────── -->
<UniformGrid DockPanel.Dock="Bottom" Rows="1" Columns="2" Margin="0,10,0,0">
<!-- PSG -->
<Border Margin="0,0,4,0"
Background="{DynamicResource ControlFillColorSecondaryBrush}"
BorderBrush="{DynamicResource ControlStrokeColorDefaultBrush}"
BorderThickness="1" CornerRadius="6" Padding="10,8">
<StackPanel>
<TextBlock Text="PSG" FontSize="10" FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Margin="0,0,0,2"/>
<TextBlock Text="{Binding AngleDisplay.PsgRelativeAngle}"
Foreground="{Binding AngleDisplay.PsgAngleForeground}"
FontFamily="Consolas" FontSize="20" FontWeight="SemiBold"/>
<TextBlock Text="{Binding AngleDisplay.PsgEncoderAngle}"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
FontFamily="Consolas" FontSize="11"
Margin="0,2,0,0"/>
</StackPanel>
</Border>
<!-- INJ -->
<Border Margin="4,0,0,0"
Background="{DynamicResource ControlFillColorSecondaryBrush}"
BorderBrush="{DynamicResource ControlStrokeColorDefaultBrush}"
BorderThickness="1" CornerRadius="6" Padding="10,8">
<StackPanel>
<TextBlock Text="INJ" FontSize="10" FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Margin="0,0,0,2"/>
<TextBlock Text="{Binding AngleDisplay.InjRelativeAngle}"
Foreground="{Binding AngleDisplay.InjAngleForeground}"
FontFamily="Consolas" FontSize="20" FontWeight="SemiBold"/>
<TextBlock Text="{Binding AngleDisplay.InjEncoderAngle}"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
FontFamily="Consolas" FontSize="11"
Margin="0,2,0,0"/>
</StackPanel>
</Border>
</UniformGrid>
<!-- ── Radial gauge (fills remaining space) ────────────────── -->
<ctrl:RadialAngleGauge
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
MinHeight="200"
PrimaryAngle="{Binding AngleDisplay.PrimaryGaugeAngle}"
TargetAngle="{Binding AngleDisplay.TargetAngleForGauge}"
SecondaryAngle="{Binding AngleDisplay.SecondaryGaugeAngle}"
PrimaryBrush="{Binding AngleDisplay.PsgAngleForeground}"
TargetBrush="{Binding AngleDisplay.LockAngleForeground}"
IsRunningMode="{Binding AngleDisplay.IsRunningMode}"/>
</DockPanel>
</Border>
</UserControl>

View File

@@ -0,0 +1,14 @@
using System.Windows.Controls;
namespace HC_APTBS.Views.UserControls
{
/// <summary>
/// Fluent card hosting the radial advance angle gauge, PSG/INJ readouts,
/// lock-offset input, and set-zero buttons.
/// DataContext = <see cref="HC_APTBS.ViewModels.Pages.BenchPageViewModel"/>.
/// </summary>
public partial class AdvanceMonitorCard : UserControl
{
public AdvanceMonitorCard() => InitializeComponent();
}
}

View File

@@ -0,0 +1,219 @@
<UserControl x:Class="HC_APTBS.Views.UserControls.BenchActuatorsCard"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
mc:Ignorable="d"
d:DesignHeight="560" d:DesignWidth="320">
<!--
Actuators & Relays card — sub-sections for direction, oil pump + counter,
temperature control, and misc relays.
DataContext = BenchPageViewModel (binds BenchControl.*, TempControl.*, RelayBank.*).
-->
<Border Style="{StaticResource PumpCard}">
<StackPanel>
<!-- ── Card header ─────────────────────────────────────────── -->
<DockPanel Margin="0,0,0,12">
<ui:SymbolIcon DockPanel.Dock="Left" Symbol="ToggleLeft24" FontSize="16"
Foreground="{DynamicResource AccentTextFillColorPrimaryBrush}"
Margin="0,0,8,0" VerticalAlignment="Center"/>
<TextBlock Text="{DynamicResource Bench.Actuators.Title}"
Style="{StaticResource PumpCardHeader}" Margin="0"/>
</DockPanel>
<!-- ── Direction ──────────────────────────────────────────── -->
<TextBlock Text="{DynamicResource Bench.Actuators.Direction}"
FontSize="11" FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Margin="0,0,0,6"/>
<Grid Margin="0,0,0,12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- Right — IsChecked=OneWay shows active state; Command performs the change -->
<ToggleButton Grid.Column="0" Height="40"
IsChecked="{Binding BenchControl.IsDirectionRight, Mode=OneWay}"
Command="{Binding BenchControl.SetDirectionRightCommand}"
Style="{StaticResource FluentStateToggle}">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<ui:SymbolIcon Symbol="ArrowRight24" FontSize="14" Margin="0,0,4,0"/>
<TextBlock Text="{DynamicResource Bench.Actuators.DirRight}" VerticalAlignment="Center"/>
</StackPanel>
</ToggleButton>
<!-- Left — IsDirectionLeft is the computed inverse of IsDirectionRight -->
<ToggleButton Grid.Column="2" Height="40"
IsChecked="{Binding BenchControl.IsDirectionLeft, Mode=OneWay}"
Command="{Binding BenchControl.SetDirectionLeftCommand}"
Style="{StaticResource FluentStateToggle}">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<ui:SymbolIcon Symbol="ArrowLeft24" FontSize="14" Margin="0,0,4,0"/>
<TextBlock Text="{DynamicResource Bench.Actuators.DirLeft}" VerticalAlignment="Center"/>
</StackPanel>
</ToggleButton>
</Grid>
<!-- ── Divider ─────────────────────────────────────────────── -->
<Border Height="1" Background="{DynamicResource ControlStrokeColorDefaultBrush}"
Margin="0,0,0,10"/>
<!-- ── Oil pump + Counter ──────────────────────────────────── -->
<TextBlock Text="{DynamicResource Bench.Actuators.OilPump}"
FontSize="11" FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Margin="0,0,0,6"/>
<Grid Margin="0,0,0,8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<ToggleButton Grid.Column="0" Height="40"
IsChecked="{Binding BenchControl.IsOilPumpOn}"
Style="{StaticResource FluentStateToggle}">
<StackPanel Orientation="Horizontal">
<ui:SymbolIcon Symbol="Drop24" FontSize="14" Margin="0,0,4,0"/>
<TextBlock Text="{DynamicResource Bench.Actuators.OilPump}" VerticalAlignment="Center"/>
</StackPanel>
</ToggleButton>
<Border Grid.Column="2" Background="{DynamicResource ControlFillColorSecondaryBrush}"
BorderBrush="{DynamicResource ControlStrokeColorDefaultBrush}"
BorderThickness="1" CornerRadius="6" Padding="8,6">
<StackPanel>
<TextBlock Text="{DynamicResource Bench.Actuators.Counter}"
FontSize="10"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Margin="0,0,0,2"/>
<TextBlock Text="{Binding BenchControl.BenchCounterValue, StringFormat=F0}"
FontFamily="Consolas" FontSize="18" FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"/>
</StackPanel>
</Border>
</Grid>
<!-- Counter input row -->
<Grid Margin="0,0,0,12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0"
Text="{Binding BenchControl.CounterInputText, UpdateSourceTrigger=PropertyChanged}"
FontFamily="Consolas" FontSize="16"
Height="36" VerticalContentAlignment="Center"
HorizontalContentAlignment="Right" Padding="8,0"/>
<ui:Button Grid.Column="2" Height="36"
Content="{DynamicResource Bench.Actuators.Set}"
Command="{Binding BenchControl.SendCounterCommand}"
Appearance="Secondary"/>
</Grid>
<!-- ── Divider ─────────────────────────────────────────────── -->
<Border Height="1" Background="{DynamicResource ControlStrokeColorDefaultBrush}"
Margin="0,0,0,10"/>
<!-- ── Temperature control ─────────────────────────────────── -->
<TextBlock Text="{DynamicResource Bench.Actuators.Temperature}"
FontSize="11" FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Margin="0,0,0,6"/>
<!-- Setpoint + tolerance input -->
<Grid Margin="0,0,0,8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="6"/>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0"
Text="{Binding TempControl.SetpointText, UpdateSourceTrigger=PropertyChanged}"
ToolTip="{DynamicResource Bench.Actuators.Setpoint}"
FontFamily="Consolas" FontSize="16"
Height="36" VerticalContentAlignment="Center"
HorizontalContentAlignment="Right" Padding="8,0"/>
<TextBlock Grid.Column="0" Text="°C" FontSize="11"
VerticalAlignment="Center" HorizontalAlignment="Right"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Margin="0,0,4,0" IsHitTestVisible="False"/>
<TextBox Grid.Column="2"
Text="{Binding TempControl.ToleranceText, UpdateSourceTrigger=PropertyChanged}"
ToolTip="{DynamicResource Bench.Actuators.Tolerance}"
FontFamily="Consolas" FontSize="16"
Height="36" VerticalContentAlignment="Center"
HorizontalContentAlignment="Right" Padding="8,0"/>
<ui:Button Grid.Column="4" Height="36"
Content="{DynamicResource Bench.RpmCommand.Apply}"
Command="{Binding TempControl.ApplySetpointCommand}"
Appearance="Secondary"/>
</Grid>
<!-- Heater / Deposit Cooler / T-in Cooler toggles -->
<UniformGrid Rows="1" Columns="3" Margin="0,0,0,12">
<ToggleButton Height="38" Margin="0,0,2,0"
IsChecked="{Binding TempControl.IsHeaterOn}"
Style="{StaticResource FluentStateToggle}">
<StackPanel Orientation="Horizontal">
<ui:SymbolIcon Symbol="Fire16" FontSize="13" Margin="0,0,3,0"/>
<TextBlock Text="{DynamicResource Bench.Actuators.Heater}" VerticalAlignment="Center" FontSize="11"/>
</StackPanel>
</ToggleButton>
<ToggleButton Height="38" Margin="2,0,2,0"
IsChecked="{Binding TempControl.IsDepositCoolerOn}"
Style="{StaticResource FluentStateToggle}">
<StackPanel Orientation="Horizontal">
<ui:SymbolIcon Symbol="ArrowDown16" FontSize="13" Margin="0,0,3,0"/>
<TextBlock Text="{DynamicResource Bench.Actuators.DepositCooler}" VerticalAlignment="Center" FontSize="11"/>
</StackPanel>
</ToggleButton>
<ToggleButton Height="38" Margin="2,0,0,0"
IsChecked="{Binding TempControl.IsTinCoolerOn}"
Style="{StaticResource FluentStateToggle}">
<StackPanel Orientation="Horizontal">
<ui:SymbolIcon Symbol="Drop16" FontSize="13" Margin="0,0,3,0"/>
<TextBlock Text="{DynamicResource Bench.Actuators.TinCooler}" VerticalAlignment="Center" FontSize="11"/>
</StackPanel>
</ToggleButton>
</UniformGrid>
<!-- ── Divider ─────────────────────────────────────────────── -->
<Border Height="1" Background="{DynamicResource ControlStrokeColorDefaultBrush}"
Margin="0,0,0,10"/>
<!-- ── Misc relays ─────────────────────────────────────────── -->
<TextBlock Text="{DynamicResource Bench.Actuators.Relays}"
FontSize="11" FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Margin="0,0,0,6"/>
<UniformGrid Rows="1" Columns="3">
<ToggleButton Height="38" Margin="0,0,2,0"
IsChecked="{Binding RelayBank.IsElectronicOn}"
Style="{StaticResource FluentStateToggle}">
<TextBlock Text="{DynamicResource Bench.Actuators.Electronic}" FontSize="11"
VerticalAlignment="Center" HorizontalAlignment="Center"/>
</ToggleButton>
<ToggleButton Height="38" Margin="2,0,2,0"
IsChecked="{Binding RelayBank.IsFlasherOn}"
Style="{StaticResource FluentStateToggle}">
<TextBlock Text="{DynamicResource Bench.Actuators.Flasher}" FontSize="11"
VerticalAlignment="Center" HorizontalAlignment="Center"/>
</ToggleButton>
<ToggleButton Height="38" Margin="2,0,0,0"
IsChecked="{Binding RelayBank.IsPulse4SignalOn}"
Style="{StaticResource FluentStateToggle}">
<TextBlock Text="{DynamicResource Bench.Actuators.Pulse4}" FontSize="11"
VerticalAlignment="Center" HorizontalAlignment="Center"/>
</ToggleButton>
</UniformGrid>
</StackPanel>
</Border>
</UserControl>

View File

@@ -0,0 +1,14 @@
using System.Windows.Controls;
namespace HC_APTBS.Views.UserControls
{
/// <summary>
/// Fluent card grouping all bench actuator controls: oil pump, counter, direction,
/// temperature PID, and auxiliary relays.
/// DataContext = <see cref="HC_APTBS.ViewModels.Pages.BenchPageViewModel"/>.
/// </summary>
public partial class BenchActuatorsCard : UserControl
{
public BenchActuatorsCard() => InitializeComponent();
}
}

View File

@@ -0,0 +1,93 @@
<UserControl x:Class="HC_APTBS.Views.UserControls.BenchChartsCard"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:uc="clr-namespace:HC_APTBS.Views.UserControls"
mc:Ignorable="d"
d:DesignHeight="380" d:DesignWidth="560">
<!--
Live Charts card — 2×2 grid of compact chart tiles.
Order: TL = Delivery, TR = Over, BL = P1, BR = P2.
DataContext = BenchPageViewModel.
-->
<Border Style="{StaticResource PumpCard}">
<DockPanel LastChildFill="True">
<!-- ── Card header ─────────────────────────────────────────── -->
<DockPanel DockPanel.Dock="Top" Margin="0,0,0,10">
<ui:SymbolIcon DockPanel.Dock="Left" Symbol="ChartMultiple24" FontSize="16"
Foreground="{DynamicResource AccentTextFillColorPrimaryBrush}"
Margin="0,0,8,0" VerticalAlignment="Center"/>
<TextBlock Text="{DynamicResource Bench.Charts.Title}"
Style="{StaticResource PumpCardHeader}" Margin="0"/>
</DockPanel>
<!-- ── 2×2 chart grid ──────────────────────────────────────── -->
<UniformGrid Rows="2" Columns="2">
<!-- TL: Q Delivery -->
<Border Background="{DynamicResource ControlFillColorSecondaryBrush}"
BorderBrush="{DynamicResource ControlStrokeColorDefaultBrush}"
BorderThickness="1" CornerRadius="6" Margin="0,0,4,4" Padding="8,6">
<DockPanel>
<TextBlock DockPanel.Dock="Top"
Text="{DynamicResource Bench.Chart.Delivery}"
FontSize="11" FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Margin="0,0,0,4"/>
<uc:FlowmeterChartView IsCompact="True"
DataContext="{Binding FlowmeterChart.Delivery}"/>
</DockPanel>
</Border>
<!-- TR: Q Over -->
<Border Background="{DynamicResource ControlFillColorSecondaryBrush}"
BorderBrush="{DynamicResource ControlStrokeColorDefaultBrush}"
BorderThickness="1" CornerRadius="6" Margin="4,0,0,4" Padding="8,6">
<DockPanel>
<TextBlock DockPanel.Dock="Top"
Text="{DynamicResource Bench.Chart.Over}"
FontSize="11" FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Margin="0,0,0,4"/>
<uc:FlowmeterChartView IsCompact="True"
DataContext="{Binding FlowmeterChart.Over}"/>
</DockPanel>
</Border>
<!-- BL: Pressure P1 -->
<Border Background="{DynamicResource ControlFillColorSecondaryBrush}"
BorderBrush="{DynamicResource ControlStrokeColorDefaultBrush}"
BorderThickness="1" CornerRadius="6" Margin="0,4,4,0" Padding="8,6">
<DockPanel>
<TextBlock DockPanel.Dock="Top"
Text="{DynamicResource Bench.Chart.P1}"
FontSize="11" FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Margin="0,0,0,4"/>
<uc:FlowmeterChartView IsCompact="True"
DataContext="{Binding PressureTrace.P1}"/>
</DockPanel>
</Border>
<!-- BR: Pressure P2 -->
<Border Background="{DynamicResource ControlFillColorSecondaryBrush}"
BorderBrush="{DynamicResource ControlStrokeColorDefaultBrush}"
BorderThickness="1" CornerRadius="6" Margin="4,4,0,0" Padding="8,6">
<DockPanel>
<TextBlock DockPanel.Dock="Top"
Text="{DynamicResource Bench.Chart.P2}"
FontSize="11" FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Margin="0,0,0,4"/>
<uc:FlowmeterChartView IsCompact="True"
DataContext="{Binding PressureTrace.P2}"/>
</DockPanel>
</Border>
</UniformGrid>
</DockPanel>
</Border>
</UserControl>

View File

@@ -0,0 +1,14 @@
using System.Windows.Controls;
namespace HC_APTBS.Views.UserControls
{
/// <summary>
/// Fluent card with a 2×2 grid of compact real-time bench charts
/// (Q-Delivery, Q-Over, P1, P2).
/// DataContext = <see cref="HC_APTBS.ViewModels.Pages.BenchPageViewModel"/>.
/// </summary>
public partial class BenchChartsCard : UserControl
{
public BenchChartsCard() => InitializeComponent();
}
}

View File

@@ -0,0 +1,235 @@
<UserControl x:Class="HC_APTBS.Views.UserControls.BenchLiveDataCard"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="800">
<!--
Live bench readings as two rows of five KPI tiles.
Row 1: RPM, P1, P2, Q-Delivery, Q-Over.
Row 2: T-In, T-Out, T4, T-Tank, Bench Temp.
DataContext = BenchPageViewModel.
-->
<UserControl.Resources>
<Style x:Key="BenchKpiTile" TargetType="Border" BasedOn="{StaticResource KpiTile}">
<Setter Property="MinHeight" Value="100"/>
</Style>
</UserControl.Resources>
<Border Style="{StaticResource PumpCard}">
<DockPanel LastChildFill="True">
<!-- ── Card header ─────────────────────────────────────────── -->
<DockPanel DockPanel.Dock="Top" Margin="0,0,0,10">
<ui:SymbolIcon DockPanel.Dock="Left" Symbol="DataLine24" FontSize="16"
Foreground="{DynamicResource AccentTextFillColorPrimaryBrush}"
Margin="0,0,8,0" VerticalAlignment="Center"/>
<TextBlock Text="{DynamicResource Bench.LiveData.Title}"
Style="{StaticResource PumpCardHeader}" Margin="0"/>
</DockPanel>
<!-- ── Row 2: T-In, T-Out, T4, T-Tank, Bench Temp ─────────── -->
<UniformGrid DockPanel.Dock="Bottom" Rows="1" Columns="5" Margin="0,6,0,0">
<Border Style="{StaticResource BenchKpiTile}">
<Grid>
<Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="*"/></Grid.RowDefinitions>
<StackPanel Orientation="Horizontal">
<ui:SymbolIcon Symbol="Temperature24" FontSize="13"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Margin="0,0,4,0" VerticalAlignment="Center"/>
<TextBlock Text="{DynamicResource Bench.Kpi.TIn}"
Style="{StaticResource KpiHeaderText}" VerticalAlignment="Center"/>
</StackPanel>
<StackPanel Grid.Row="1" Orientation="Horizontal" VerticalAlignment="Bottom">
<TextBlock Text="{Binding Root.TempIn, StringFormat=F1}"
Style="{StaticResource KpiValueText}" FontSize="28"/>
<TextBlock Text="{DynamicResource Pump.UnitCelsius}"
Style="{StaticResource KpiUnitText}"/>
</StackPanel>
</Grid>
</Border>
<Border Style="{StaticResource BenchKpiTile}">
<Grid>
<Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="*"/></Grid.RowDefinitions>
<StackPanel Orientation="Horizontal">
<ui:SymbolIcon Symbol="Temperature24" FontSize="13"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Margin="0,0,4,0" VerticalAlignment="Center"/>
<TextBlock Text="{DynamicResource Bench.Kpi.TOut}"
Style="{StaticResource KpiHeaderText}" VerticalAlignment="Center"/>
</StackPanel>
<StackPanel Grid.Row="1" Orientation="Horizontal" VerticalAlignment="Bottom">
<TextBlock Text="{Binding Root.TempOut, StringFormat=F1}"
Style="{StaticResource KpiValueText}" FontSize="28"/>
<TextBlock Text="{DynamicResource Pump.UnitCelsius}"
Style="{StaticResource KpiUnitText}"/>
</StackPanel>
</Grid>
</Border>
<Border Style="{StaticResource BenchKpiTile}">
<Grid>
<Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="*"/></Grid.RowDefinitions>
<StackPanel Orientation="Horizontal">
<ui:SymbolIcon Symbol="Temperature24" FontSize="13"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Margin="0,0,4,0" VerticalAlignment="Center"/>
<TextBlock Text="{DynamicResource Bench.Kpi.T4}"
Style="{StaticResource KpiHeaderText}" VerticalAlignment="Center"/>
</StackPanel>
<StackPanel Grid.Row="1" Orientation="Horizontal" VerticalAlignment="Bottom">
<TextBlock Text="{Binding Root.Temp4, StringFormat=F1}"
Style="{StaticResource KpiValueText}" FontSize="28"/>
<TextBlock Text="{DynamicResource Pump.UnitCelsius}"
Style="{StaticResource KpiUnitText}"/>
</StackPanel>
</Grid>
</Border>
<Border Style="{StaticResource BenchKpiTile}">
<Grid>
<Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="*"/></Grid.RowDefinitions>
<StackPanel Orientation="Horizontal">
<ui:SymbolIcon Symbol="Temperature24" FontSize="13"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Margin="0,0,4,0" VerticalAlignment="Center"/>
<TextBlock Text="{DynamicResource Bench.Kpi.TTank}"
Style="{StaticResource KpiHeaderText}" VerticalAlignment="Center"/>
</StackPanel>
<StackPanel Grid.Row="1" Orientation="Horizontal" VerticalAlignment="Bottom">
<TextBlock Text="{Binding Root.BenchTemp, StringFormat=F1}"
Style="{StaticResource KpiValueText}" FontSize="28"/>
<TextBlock Text="{DynamicResource Pump.UnitCelsius}"
Style="{StaticResource KpiUnitText}"/>
</StackPanel>
</Grid>
</Border>
<Border Style="{StaticResource BenchKpiTile}">
<Grid>
<Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="*"/></Grid.RowDefinitions>
<StackPanel Orientation="Horizontal">
<ui:SymbolIcon Symbol="Temperature24" FontSize="13"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Margin="0,0,4,0" VerticalAlignment="Center"/>
<TextBlock Text="{DynamicResource Bench.Kpi.BenchTemp}"
Style="{StaticResource KpiHeaderText}" VerticalAlignment="Center"/>
</StackPanel>
<StackPanel Grid.Row="1" Orientation="Horizontal" VerticalAlignment="Bottom">
<TextBlock Text="{Binding Root.BenchTemp, StringFormat=F1}"
Style="{StaticResource KpiValueText}" FontSize="28"/>
<TextBlock Text="{DynamicResource Pump.UnitCelsius}"
Style="{StaticResource KpiUnitText}"/>
</StackPanel>
</Grid>
</Border>
</UniformGrid>
<!-- ── Row 1: RPM, P1, P2, Q-Delivery, Q-Over ─────────────── -->
<UniformGrid Rows="1" Columns="5">
<Border Style="{StaticResource BenchKpiTile}">
<Grid>
<Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="*"/></Grid.RowDefinitions>
<StackPanel Orientation="Horizontal">
<ui:SymbolIcon Symbol="Gauge24" FontSize="13"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Margin="0,0,4,0" VerticalAlignment="Center"/>
<TextBlock Text="{DynamicResource Bench.Kpi.Rpm}"
Style="{StaticResource KpiHeaderText}" VerticalAlignment="Center"/>
</StackPanel>
<StackPanel Grid.Row="1" Orientation="Horizontal" VerticalAlignment="Bottom">
<TextBlock Text="{Binding Root.BenchRpm, StringFormat=F0}"
Style="{StaticResource KpiValueText}" FontSize="28"/>
<TextBlock Text="{DynamicResource Bench.Rpm}"
Style="{StaticResource KpiUnitText}"/>
</StackPanel>
</Grid>
</Border>
<Border Style="{StaticResource BenchKpiTile}">
<Grid>
<Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="*"/></Grid.RowDefinitions>
<StackPanel Orientation="Horizontal">
<ui:SymbolIcon Symbol="ArrowTrendingLines24" FontSize="13"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Margin="0,0,4,0" VerticalAlignment="Center"/>
<TextBlock Text="{DynamicResource Bench.Kpi.P1}"
Style="{StaticResource KpiHeaderText}" VerticalAlignment="Center"/>
</StackPanel>
<StackPanel Grid.Row="1" Orientation="Horizontal" VerticalAlignment="Bottom">
<TextBlock Text="{Binding Root.Pressure, StringFormat=F2}"
Style="{StaticResource KpiValueText}" FontSize="28"/>
<TextBlock Text="{DynamicResource Bench.Kpi.Unit.Bar}"
Style="{StaticResource KpiUnitText}"/>
</StackPanel>
</Grid>
</Border>
<Border Style="{StaticResource BenchKpiTile}">
<Grid>
<Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="*"/></Grid.RowDefinitions>
<StackPanel Orientation="Horizontal">
<ui:SymbolIcon Symbol="ArrowTrendingLines24" FontSize="13"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Margin="0,0,4,0" VerticalAlignment="Center"/>
<TextBlock Text="{DynamicResource Bench.Kpi.P2}"
Style="{StaticResource KpiHeaderText}" VerticalAlignment="Center"/>
</StackPanel>
<StackPanel Grid.Row="1" Orientation="Horizontal" VerticalAlignment="Bottom">
<TextBlock Text="{Binding Root.Pressure2, StringFormat=F2}"
Style="{StaticResource KpiValueText}" FontSize="28"/>
<TextBlock Text="{DynamicResource Bench.Kpi.Unit.Bar}"
Style="{StaticResource KpiUnitText}"/>
</StackPanel>
</Grid>
</Border>
<Border Style="{StaticResource BenchKpiTile}">
<Grid>
<Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="*"/></Grid.RowDefinitions>
<StackPanel Orientation="Horizontal">
<ui:SymbolIcon Symbol="Drop24" FontSize="13"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Margin="0,0,4,0" VerticalAlignment="Center"/>
<TextBlock Text="{DynamicResource Bench.Kpi.QDelivery}"
Style="{StaticResource KpiHeaderText}" VerticalAlignment="Center"/>
</StackPanel>
<StackPanel Grid.Row="1" Orientation="Horizontal" VerticalAlignment="Bottom">
<TextBlock Text="{Binding Root.QDelivery, StringFormat=F1}"
Style="{StaticResource KpiValueText}" FontSize="28"/>
<TextBlock Text="{DynamicResource Bench.Kpi.Unit.CcS}"
Style="{StaticResource KpiUnitText}"/>
</StackPanel>
</Grid>
</Border>
<Border Style="{StaticResource BenchKpiTile}">
<Grid>
<Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="*"/></Grid.RowDefinitions>
<StackPanel Orientation="Horizontal">
<ui:SymbolIcon Symbol="Drop24" FontSize="13"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Margin="0,0,4,0" VerticalAlignment="Center"/>
<TextBlock Text="{DynamicResource Bench.Kpi.QOver}"
Style="{StaticResource KpiHeaderText}" VerticalAlignment="Center"/>
</StackPanel>
<StackPanel Grid.Row="1" Orientation="Horizontal" VerticalAlignment="Bottom">
<TextBlock Text="{Binding Root.QOver, StringFormat=F1}"
Style="{StaticResource KpiValueText}" FontSize="28"/>
<TextBlock Text="{DynamicResource Bench.Kpi.Unit.CcS}"
Style="{StaticResource KpiUnitText}"/>
</StackPanel>
</Grid>
</Border>
</UniformGrid>
</DockPanel>
</Border>
</UserControl>

View File

@@ -0,0 +1,14 @@
using System.Windows.Controls;
namespace HC_APTBS.Views.UserControls
{
/// <summary>
/// Fluent card with two rows of KPI tiles showing bench live readings
/// (RPM, pressures, flow, temperatures).
/// DataContext = <see cref="HC_APTBS.ViewModels.Pages.BenchPageViewModel"/>.
/// </summary>
public partial class BenchLiveDataCard : UserControl
{
public BenchLiveDataCard() => InitializeComponent();
}
}

View File

@@ -0,0 +1,145 @@
<UserControl x:Class="HC_APTBS.Views.UserControls.BenchRpmCommandCard"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
mc:Ignorable="d"
d:DesignHeight="440" d:DesignWidth="320">
<!--
RPM command card — inline numeric entry, 8 preset buttons, Start/Stop.
DataContext = BenchPageViewModel.
-->
<Border Style="{StaticResource PumpCard}">
<StackPanel>
<!-- ── Card header ─────────────────────────────────────────── -->
<DockPanel Margin="0,0,0,12">
<ui:SymbolIcon DockPanel.Dock="Left" Symbol="Gauge24" FontSize="16"
Foreground="{DynamicResource AccentTextFillColorPrimaryBrush}"
Margin="0,0,8,0" VerticalAlignment="Center"/>
<TextBlock Text="{DynamicResource Bench.RpmCommand.Title}"
Style="{StaticResource PumpCardHeader}" Margin="0"/>
</DockPanel>
<!-- ── Actual / Target mini readouts ───────────────────────── -->
<Grid Margin="0,0,0,10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Border Grid.Column="0"
Background="{DynamicResource ControlFillColorSecondaryBrush}"
BorderBrush="{DynamicResource ControlStrokeColorDefaultBrush}"
BorderThickness="1" CornerRadius="6" Padding="10,8">
<StackPanel>
<TextBlock Text="{DynamicResource Bench.RpmCommand.Actual}"
FontSize="11"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Margin="0,0,0,4"/>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="{Binding Root.BenchRpm, StringFormat=F0}"
FontFamily="Consolas" FontSize="26" FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"/>
<TextBlock Text=" rpm" FontSize="11"
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
VerticalAlignment="Bottom" Margin="0,0,0,3"/>
</StackPanel>
</StackPanel>
</Border>
<Border Grid.Column="2"
Background="{DynamicResource ControlFillColorSecondaryBrush}"
BorderBrush="{DynamicResource ControlStrokeColorDefaultBrush}"
BorderThickness="1" CornerRadius="6" Padding="10,8">
<StackPanel>
<TextBlock Text="{DynamicResource Bench.RpmCommand.Target}"
FontSize="11"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Margin="0,0,0,4"/>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="{Binding BenchControl.TargetRpm, StringFormat=F0}"
FontFamily="Consolas" FontSize="26" FontWeight="SemiBold"
Foreground="{DynamicResource AccentTextFillColorPrimaryBrush}"/>
<TextBlock Text=" rpm" FontSize="11"
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
VerticalAlignment="Bottom" Margin="0,0,0,3"/>
</StackPanel>
<TextBlock Text="{Binding BenchControl.CommandVoltage, StringFormat='F3\u202FV'}"
FontFamily="Consolas" FontSize="10"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"/>
</StackPanel>
</Border>
</Grid>
<!-- ── Manual RPM input ─────────────────────────────────────── -->
<Grid Margin="0,0,0,8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0"
Text="{Binding BenchControl.RpmInputText, UpdateSourceTrigger=PropertyChanged}"
FontFamily="Consolas" FontSize="20" FontWeight="SemiBold"
Height="40" VerticalContentAlignment="Center"
HorizontalContentAlignment="Right" Padding="8,0"/>
<TextBlock Grid.Column="1" Text="rpm"
VerticalAlignment="Center"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
FontSize="12" Margin="6,0,0,0"/>
</Grid>
<!-- ── Preset RPM buttons 2×4 ──────────────────────────────── -->
<UniformGrid Rows="2" Columns="4" Margin="0,0,0,10">
<ui:Button Content="100" CommandParameter="100"
Command="{Binding BenchControl.SetQuickRpmCommand}"
Appearance="Secondary" Height="40" Margin="2" FontSize="12"/>
<ui:Button Content="200" CommandParameter="200"
Command="{Binding BenchControl.SetQuickRpmCommand}"
Appearance="Secondary" Height="40" Margin="2" FontSize="12"/>
<ui:Button Content="500" CommandParameter="500"
Command="{Binding BenchControl.SetQuickRpmCommand}"
Appearance="Secondary" Height="40" Margin="2" FontSize="12"/>
<ui:Button Content="750" CommandParameter="750"
Command="{Binding BenchControl.SetQuickRpmCommand}"
Appearance="Secondary" Height="40" Margin="2" FontSize="12"/>
<ui:Button Content="1000" CommandParameter="1000"
Command="{Binding BenchControl.SetQuickRpmCommand}"
Appearance="Secondary" Height="40" Margin="2" FontSize="12"/>
<ui:Button Content="1250" CommandParameter="1250"
Command="{Binding BenchControl.SetQuickRpmCommand}"
Appearance="Secondary" Height="40" Margin="2" FontSize="12"/>
<ui:Button Content="1500" CommandParameter="1500"
Command="{Binding BenchControl.SetQuickRpmCommand}"
Appearance="Secondary" Height="40" Margin="2" FontSize="12"/>
<ui:Button Content="2000" CommandParameter="2000"
Command="{Binding BenchControl.SetQuickRpmCommand}"
Appearance="Secondary" Height="40" Margin="2" FontSize="12"/>
</UniformGrid>
<!-- ── Start / Stop ─────────────────────────────────────────── -->
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<ui:Button Grid.Column="0"
Content="{DynamicResource Bench.Start}"
Command="{Binding BenchControl.StartBenchCommand}"
Appearance="Primary" Height="46" FontWeight="Bold" FontSize="14">
<ui:Button.Icon><ui:SymbolIcon Symbol="Play24"/></ui:Button.Icon>
</ui:Button>
<ui:Button Grid.Column="2"
Content="{DynamicResource Bench.Stop}"
Command="{Binding BenchControl.StopBenchCommand}"
Appearance="Danger" Height="46" FontWeight="Bold" FontSize="14">
<ui:Button.Icon><ui:SymbolIcon Symbol="Stop24"/></ui:Button.Icon>
</ui:Button>
</Grid>
</StackPanel>
</Border>
</UserControl>

View File

@@ -0,0 +1,14 @@
using System.Windows.Controls;
namespace HC_APTBS.Views.UserControls
{
/// <summary>
/// Fluent card providing RPM numeric input, preset buttons, Start/Stop and
/// voltage readout for manual bench motor control.
/// DataContext = <see cref="HC_APTBS.ViewModels.Pages.BenchPageViewModel"/>.
/// </summary>
public partial class BenchRpmCommandCard : UserControl
{
public BenchRpmCommandCard() => InitializeComponent();
}
}

View File

@@ -18,7 +18,8 @@
VerticalAlignment="Center"/> VerticalAlignment="Center"/>
</StackPanel> </StackPanel>
<lvc:CartesianChart Grid.Row="1" Height="120" <lvc:CartesianChart Grid.Row="1"
Height="{Binding ChartHeight, RelativeSource={RelativeSource AncestorType=UserControl}}"
Series="{Binding Series}" Series="{Binding Series}"
XAxes="{Binding XAxes}" XAxes="{Binding XAxes}"
YAxes="{Binding YAxes}" YAxes="{Binding YAxes}"

View File

@@ -1,3 +1,4 @@
using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
namespace HC_APTBS.Views.UserControls namespace HC_APTBS.Views.UserControls
@@ -5,9 +6,34 @@ namespace HC_APTBS.Views.UserControls
/// <summary> /// <summary>
/// UserControl hosting a single real-time flowmeter chart. /// UserControl hosting a single real-time flowmeter chart.
/// DataContext is expected to be a <see cref="HC_APTBS.ViewModels.SingleFlowChartViewModel"/>. /// DataContext is expected to be a <see cref="HC_APTBS.ViewModels.SingleFlowChartViewModel"/>.
/// Set <see cref="IsCompact"/> to <c>true</c> to reduce chart height to 90 px
/// (used in the 2×2 bench chart grid).
/// </summary> /// </summary>
public partial class FlowmeterChartView : UserControl public partial class FlowmeterChartView : UserControl
{ {
/// <summary>When true the chart height shrinks from 120 to 90 px.</summary>
public static readonly DependencyProperty IsCompactProperty =
DependencyProperty.Register(nameof(IsCompact), typeof(bool), typeof(FlowmeterChartView),
new FrameworkPropertyMetadata(false,
(d, e) => ((FlowmeterChartView)d).ChartHeight = (bool)e.NewValue ? 90.0 : 120.0));
public bool IsCompact
{
get => (bool)GetValue(IsCompactProperty);
set => SetValue(IsCompactProperty, value);
}
/// <summary>Derived chart height (120 or 90); bound in XAML.</summary>
public static readonly DependencyProperty ChartHeightProperty =
DependencyProperty.Register(nameof(ChartHeight), typeof(double), typeof(FlowmeterChartView),
new FrameworkPropertyMetadata(120.0));
public double ChartHeight
{
get => (double)GetValue(ChartHeightProperty);
set => SetValue(ChartHeightProperty, value);
}
public FlowmeterChartView() public FlowmeterChartView()
{ {
InitializeComponent(); InitializeComponent();